/*
 * 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.camel.component.olingo4;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.camel.CamelExecutionException;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.component.olingo4.api.batch.Olingo4BatchChangeRequest;
import org.apache.camel.component.olingo4.api.batch.Olingo4BatchQueryRequest;
import org.apache.camel.component.olingo4.api.batch.Olingo4BatchRequest;
import org.apache.camel.component.olingo4.api.batch.Olingo4BatchResponse;
import org.apache.camel.component.olingo4.api.batch.Operation;
import org.apache.olingo.client.api.domain.ClientCollectionValue;
import org.apache.olingo.client.api.domain.ClientComplexValue;
import org.apache.olingo.client.api.domain.ClientEntity;
import org.apache.olingo.client.api.domain.ClientEntitySet;
import org.apache.olingo.client.api.domain.ClientPrimitiveValue;
import org.apache.olingo.client.api.domain.ClientServiceDocument;
import org.apache.olingo.commons.api.Constants;
import org.apache.olingo.commons.api.edm.Edm;
import org.apache.olingo.commons.api.ex.ODataError;
import org.apache.olingo.commons.api.http.HttpStatusCode;
import org.apache.olingo.server.api.uri.queryoption.SystemQueryOptionKind;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Test class for {@link org.apache.camel.component.olingo4.api.Olingo4App}
 * APIs.
 * <p>
 * The integration test runs against using the sample OData 4.0 remote TripPin
 * service published on http://services.odata.org/TripPinRESTierService.
 * </p>
 */
public class Olingo4ComponentProducerTest extends AbstractOlingo4TestSupport {

    private static final Logger LOG = LoggerFactory.getLogger(Olingo4ComponentProducerTest.class);

    private static final String PEOPLE = "People";
    private static final String TEST_PEOPLE = "People('russellwhyte')";
    private static final String TEST_CREATE_KEY = "'lewisblack'";
    private static final String TEST_CREATE_PEOPLE = PEOPLE + "(" + TEST_CREATE_KEY + ")";
    private static final String TEST_CREATE_RESOURCE_CONTENT_ID = "1";
    private static final String TEST_UPDATE_RESOURCE_CONTENT_ID = "2";
    private static final String TEST_CREATE_JSON = "{\n" + "  \"UserName\": \"lewisblack\",\n" + "  \"FirstName\": \"Lewis\",\n" + "  \"LastName\": \"Black\"\n" + "}";
    private static final String TEST_UPDATE_JSON = "{\n" + "  \"UserName\": \"lewisblack\",\n" + "  \"FirstName\": \"Lewis\",\n" + "  \"MiddleName\": \"Black\",\n"
                                                   + "  \"LastName\": \"Black\"\n" + "}";

    @Test
    public void testRead() throws Exception {
        final Map<String, Object> headers = new HashMap<>();

        // Read metadata ($metadata) object
        final Edm metadata = (Edm)requestBodyAndHeaders("direct:readmetadata", null, headers);
        assertNotNull(metadata);
        assertEquals(1, metadata.getSchemas().size());

        // Read service document object
        final ClientServiceDocument document = (ClientServiceDocument)requestBodyAndHeaders("direct:readdocument", null, headers);

        assertNotNull(document);
        assertTrue(document.getEntitySets().size() > 1);
        LOG.info("Service document has {} entity sets", document.getEntitySets().size());

        // Read entity set of the People object
        final ClientEntitySet entities = (ClientEntitySet)requestBodyAndHeaders("direct:readentities", null, headers);
        assertNotNull(entities);
        assertEquals(5, entities.getEntities().size());

        // Read object count with query options passed through header
        final Long count = (Long)requestBodyAndHeaders("direct:readcount", null, headers);
        assertEquals(20, count.intValue());

        final ClientPrimitiveValue value = (ClientPrimitiveValue)requestBodyAndHeaders("direct:readvalue", null, headers);
        LOG.info("Client value \"{}\" has type {}", value, value.getTypeName());
        assertEquals("Male", value.asPrimitive().toString());

        final ClientPrimitiveValue singleProperty = (ClientPrimitiveValue)requestBodyAndHeaders("direct:readsingleprop", null, headers);
        assertTrue(singleProperty.isPrimitive());
        assertEquals("San Francisco International Airport", singleProperty.toString());

        final ClientComplexValue complexProperty = (ClientComplexValue)requestBodyAndHeaders("direct:readcomplexprop", null, headers);
        assertTrue(complexProperty.isComplex());
        assertEquals("San Francisco", complexProperty.get("City").getComplexValue().get("Name").getValue().toString());

        final ClientCollectionValue<?> collectionProperty = (ClientCollectionValue<?>)requestBodyAndHeaders("direct:readcollectionprop", null, headers);
        assertTrue(collectionProperty.isCollection());
        assertEquals(1, collectionProperty.size());
        Iterator<?> propIter = collectionProperty.iterator();
        Object propValueObj = propIter.next();
        assertIsInstanceOf(ClientComplexValue.class, propValueObj);
        ClientComplexValue propValue = (ClientComplexValue)propValueObj;
        assertEquals("Boise", propValue.get("City").getComplexValue().get("Name").getValue().toString());

        final ClientEntity entity = (ClientEntity)requestBodyAndHeaders("direct:readentitybyid", null, headers);
        assertNotNull(entity);
        assertEquals("Russell", entity.getProperty("FirstName").getValue().toString());

        final ClientEntity unbFuncReturn = (ClientEntity)requestBodyAndHeaders("direct:callunboundfunction", null, headers);
        assertNotNull(unbFuncReturn);
    }

    @Test
    public void testReadWithFilter() {
        // Read entity set with filter of the Airports object
        final ClientEntitySet entities = (ClientEntitySet)requestBody("direct:readwithfilter", null);

        assertNotNull(entities);
        assertEquals(1, entities.getEntities().size());
    }

    @Test
    public void testCreateUpdateDelete() throws Exception {
        final ClientEntity clientEntity = createEntity();

        ClientEntity entity = requestBody("direct:create-entity", clientEntity);
        assertNotNull(entity);
        assertEquals("Lewis", entity.getProperty("FirstName").getValue().toString());
        assertEquals("", entity.getProperty("MiddleName").getValue().toString());

        // update
        clientEntity.getProperties().add(objFactory.newPrimitiveProperty("MiddleName", objFactory.newPrimitiveValueBuilder().buildString("Lewis")));

        HttpStatusCode status = requestBody("direct:update-entity", clientEntity);
        assertNotNull("Update status", status);
        assertEquals("Update status", HttpStatusCode.NO_CONTENT.getStatusCode(), status.getStatusCode());
        LOG.info("Update entity status: {}", status);

        // delete
        status = requestBody("direct:delete-entity", null);
        assertNotNull("Delete status", status);
        assertEquals("Delete status", HttpStatusCode.NO_CONTENT.getStatusCode(), status.getStatusCode());
        LOG.info("Delete status: {}", status);

        // check for delete
        try {
            requestBody("direct:read-deleted-entity", null);
        } catch (CamelExecutionException e) {
            String causeMsg = e.getCause().getMessage();
            assertTrue(causeMsg.contains("[HTTP/1.1 404 Not Found]"));
        }
    }

    @Test
    public void testCreateUpdateDeleteFromJson() throws Exception {
        ClientEntity entity = requestBody("direct:create-entity", TEST_CREATE_JSON);
        assertNotNull(entity);
        assertEquals("Lewis", entity.getProperty("FirstName").getValue().toString());
        assertEquals("Black", entity.getProperty("LastName").getValue().toString());
        assertEquals("lewisblack", entity.getProperty("UserName").getValue().toString());
        assertEquals("", entity.getProperty("MiddleName").getValue().toString());

        // update
        HttpStatusCode status = requestBody("direct:update-entity", TEST_UPDATE_JSON);
        assertNotNull("Update status", status);
        assertEquals("Update status", HttpStatusCode.NO_CONTENT.getStatusCode(), status.getStatusCode());
        LOG.info("Update entity status: {}", status);

        // delete
        status = requestBody("direct:delete-entity", null);
        assertNotNull("Delete status", status);
        assertEquals("Delete status", HttpStatusCode.NO_CONTENT.getStatusCode(), status.getStatusCode());
        LOG.info("Delete status: {}", status);

        // check for delete
        try {
            requestBody("direct:read-deleted-entity", null);
        } catch (CamelExecutionException e) {
            String causeMsg = e.getCause().getMessage();
            assertTrue(causeMsg.contains("[HTTP/1.1 404 Not Found]"));
        }
    }

    private ClientEntity createEntity() {
        ClientEntity clientEntity = objFactory.newEntity(null);

        clientEntity.getProperties().add(objFactory.newPrimitiveProperty("UserName", objFactory.newPrimitiveValueBuilder().buildString("lewisblack")));
        clientEntity.getProperties().add(objFactory.newPrimitiveProperty("FirstName", objFactory.newPrimitiveValueBuilder().buildString("Lewis")));
        clientEntity.getProperties().add(objFactory.newPrimitiveProperty("LastName", objFactory.newPrimitiveValueBuilder().buildString("Black")));

        return clientEntity;
    }

    @Test
    public void testBatch() throws Exception {
        final List<Olingo4BatchRequest> batchParts = new ArrayList<>();

        // 1. Edm query
        batchParts.add(Olingo4BatchQueryRequest.resourcePath(Constants.METADATA).resourceUri(TEST_SERVICE_BASE_URL).build());

        // 2. Read entities
        batchParts.add(Olingo4BatchQueryRequest.resourcePath(PEOPLE).resourceUri(TEST_SERVICE_BASE_URL).build());

        // 3. Read entity
        batchParts.add(Olingo4BatchQueryRequest.resourcePath(TEST_PEOPLE).resourceUri(TEST_SERVICE_BASE_URL).build());

        // 4. Read with $top
        final HashMap<String, String> queryParams = new HashMap<>();
        queryParams.put(SystemQueryOptionKind.TOP.toString(), "5");
        batchParts.add(Olingo4BatchQueryRequest.resourcePath(PEOPLE).resourceUri(TEST_SERVICE_BASE_URL).queryParams(queryParams).build());

        // 5. Create entity
        ClientEntity clientEntity = createEntity();
        batchParts.add(Olingo4BatchChangeRequest.resourcePath(PEOPLE).resourceUri(TEST_SERVICE_BASE_URL).contentId(TEST_CREATE_RESOURCE_CONTENT_ID).operation(Operation.CREATE)
            .body(clientEntity).build());

        // 6. Update middle name in created entry
        clientEntity.getProperties().add(objFactory.newPrimitiveProperty("MiddleName", objFactory.newPrimitiveValueBuilder().buildString("Lewis")));
        batchParts.add(Olingo4BatchChangeRequest.resourcePath(TEST_CREATE_PEOPLE).resourceUri(TEST_SERVICE_BASE_URL).contentId(TEST_UPDATE_RESOURCE_CONTENT_ID)
            .operation(Operation.UPDATE).body(clientEntity).build());

        // 7. Delete entity
        batchParts.add(Olingo4BatchChangeRequest.resourcePath(TEST_CREATE_PEOPLE).resourceUri(TEST_SERVICE_BASE_URL).operation(Operation.DELETE).build());

        // 8. Read deleted entity to verify delete
        batchParts.add(Olingo4BatchQueryRequest.resourcePath(TEST_CREATE_PEOPLE).resourceUri(TEST_SERVICE_BASE_URL).build());

        // execute batch request
        final List<Olingo4BatchResponse> responseParts = requestBody("direct:batch", batchParts);
        assertNotNull("Batch response", responseParts);
        assertEquals("Batch responses expected", 8, responseParts.size());

        final Edm edm = (Edm)responseParts.get(0).getBody();
        assertNotNull(edm);
        LOG.info("Edm entity sets: {}", edm.getEntityContainer().getEntitySets());

        ClientEntitySet entitySet = (ClientEntitySet)responseParts.get(1).getBody();
        assertNotNull(entitySet);
        LOG.info("Read entities: {}", entitySet.getEntities());

        clientEntity = (ClientEntity)responseParts.get(2).getBody();
        assertNotNull(clientEntity);
        LOG.info("Read entiry properties: {}", clientEntity.getProperties());

        ClientEntitySet entitySetWithTop = (ClientEntitySet)responseParts.get(3).getBody();
        assertNotNull(entitySetWithTop);
        assertEquals(5, entitySetWithTop.getEntities().size());
        LOG.info("Read entities with $top=5: {}", entitySet.getEntities());

        clientEntity = (ClientEntity)responseParts.get(4).getBody();
        assertNotNull(clientEntity);
        LOG.info("Created entity: {}", clientEntity.getProperties());

        int statusCode = responseParts.get(5).getStatusCode();
        assertEquals(HttpStatusCode.NO_CONTENT.getStatusCode(), statusCode);
        LOG.info("Update MdiddleName status: {}", statusCode);

        statusCode = responseParts.get(6).getStatusCode();
        assertEquals(HttpStatusCode.NO_CONTENT.getStatusCode(), statusCode);
        LOG.info("Delete entity status: {}", statusCode);

        assertEquals(HttpStatusCode.NOT_FOUND.getStatusCode(), responseParts.get(7).getStatusCode());
        final ODataError error = (ODataError)responseParts.get(7).getBody();
        assertNotNull(error);
        LOG.info("Read deleted entity error: {}", error.getMessage());
    }

    @Test
    public void testUnboundActionRequest() throws Exception {
        final HttpStatusCode status = requestBody("direct:unbound-action-ResetDataSource", null);
        assertEquals(HttpStatusCode.NO_CONTENT.getStatusCode(), status.getStatusCode());
    }

    @Test
    public void testBoundActionRequest() throws Exception {
        final ClientEntity clientEntity = objFactory.newEntity(null);
        clientEntity.getProperties().add(objFactory.newPrimitiveProperty("userName", objFactory.newPrimitiveValueBuilder().buildString("scottketchum")));
        clientEntity.getProperties().add(objFactory.newPrimitiveProperty("tripId", objFactory.newPrimitiveValueBuilder().buildInt32(0)));

        final HttpStatusCode status = requestBody("direct:bound-action-people", clientEntity);
        assertEquals(HttpStatusCode.NO_CONTENT.getStatusCode(), status.getStatusCode());
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testEndpointHttpHeaders() throws Exception {
        final Map<String, Object> headers = new HashMap<>();
        final ClientEntity entity = (ClientEntity)requestBodyAndHeaders("direct:read-etag", null, headers);

        MockEndpoint mockEndpoint = getMockEndpoint("mock:check-etag-header");
        mockEndpoint.expectedMessageCount(1);
        mockEndpoint.assertIsSatisfied();

        Map<String, String> responseHttpHeaders = (Map<String, String>)mockEndpoint.getExchanges().get(0).getIn().getHeader("CamelOlingo4.responseHttpHeaders");
        assertEquals(responseHttpHeaders.get("ETag"), entity.getETag());

        Map<String, String> endpointHttpHeaders = new HashMap<>();
        endpointHttpHeaders.put("If-Match", entity.getETag());
        headers.put("CamelOlingo4.endpointHttpHeaders", endpointHttpHeaders);
        requestBodyAndHeaders("direct:delete-with-etag", null, headers);

        // check for deleted entity with ETag
        try {
            requestBody("direct:read-etag", null);
        } catch (CamelExecutionException e) {
            assertStringContains(e.getCause().getMessage(), "The request resource is not found.");
        }
    }

    /**
     * Read entity set of the People object and with no filter already seen, all
     * items should be present in each message
     *
     * @throws Exception
     */
    @Test
    public void testProducerReadNoFilterAlreadySeen() throws Exception {
        final Map<String, Object> headers = new HashMap<>();
        String endpoint = "direct:read-people-nofilterseen";
        int expectedEntities = 20;
        int expectedMsgCount = 3;

        MockEndpoint mockEndpoint = getMockEndpoint("mock:producer-noalreadyseen");
        mockEndpoint.expectedMessageCount(expectedMsgCount);

        for (int i = 0; i < expectedMsgCount; ++i) {
            final ClientEntitySet entities = (ClientEntitySet)requestBodyAndHeaders(endpoint, null, headers);
            assertNotNull(entities);
        }

        mockEndpoint.assertIsSatisfied();

        for (int i = 0; i < expectedMsgCount; ++i) {
            Object body = mockEndpoint.getExchanges().get(i).getIn().getBody();
            assertTrue(body instanceof ClientEntitySet);
            ClientEntitySet set = (ClientEntitySet)body;

            //
            // All messages contained all the entities
            //
            assertEquals(expectedEntities, set.getEntities().size());
        }
    }

    /**
     * Read entity set of the People object and filter already seen items on
     * subsequent exchanges
     */
    @Test
    public void testProducerReadFilterAlreadySeen() throws Exception {
        final Map<String, Object> headers = new HashMap<>();
        String endpoint = "direct:read-people-filterseen";
        int expectedEntities = 20;
        int expectedMsgCount = 3;

        MockEndpoint mockEndpoint = getMockEndpoint("mock:producer-alreadyseen");
        mockEndpoint.expectedMessageCount(expectedMsgCount);

        for (int i = 0; i < expectedMsgCount; ++i) {
            final ClientEntitySet entities = (ClientEntitySet)requestBodyAndHeaders(endpoint, null, headers);
            assertNotNull(entities);
        }

        mockEndpoint.assertIsSatisfied();

        for (int i = 0; i < expectedMsgCount; ++i) {
            Object body = mockEndpoint.getExchanges().get(i).getIn().getBody();
            assertTrue(body instanceof ClientEntitySet);
            ClientEntitySet set = (ClientEntitySet)body;

            if (i == 0) {
                //
                // First polled messages contained all the entities
                //
                assertEquals(expectedEntities, set.getEntities().size());
            } else {
                //
                // Subsequent messages should be empty
                // since the filterAlreadySeen property is true
                //
                assertEquals(0, set.getEntities().size());
            }
        }
    }

    @Override
    protected RouteBuilder createRouteBuilder() throws Exception {
        return new RouteBuilder() {
            public void configure() {
                // test routes for read
                from("direct:readmetadata").to("olingo4://read/$metadata");

                from("direct:readdocument").to("olingo4://read/");

                from("direct:readentities").to("olingo4://read/" + PEOPLE + "?$top=5&$orderby=FirstName asc");

                from("direct:readcount").to("olingo4://read/" + PEOPLE + "/$count");

                from("direct:readvalue").to("olingo4://read/" + TEST_PEOPLE + "/Gender/$value");

                from("direct:readsingleprop").to("olingo4://read/Airports('KSFO')/Name");

                from("direct:readcomplexprop").to("olingo4://read/Airports('KSFO')/Location");

                from("direct:readcollectionprop").to("olingo4://read/" + TEST_PEOPLE + "/AddressInfo");

                from("direct:readentitybyid").to("olingo4://read/" + TEST_PEOPLE + "");

                from("direct:readwithfilter").to("olingo4://read/Airports?$filter=Name eq 'San Francisco International Airport'");

                from("direct:callunboundfunction").to("olingo4://read/GetNearestAirport(lat=33,lon=-118)");

                // test route for create individual entity
                from("direct:create-entity").to("olingo4://create/" + PEOPLE);

                // test route for update
                from("direct:update-entity").to("olingo4://update/" + PEOPLE + "('lewisblack')");

                // test route for delete
                from("direct:delete-entity").to("olingo4://delete/" + PEOPLE + "('lewisblack')");

                // test route for delete
                from("direct:read-deleted-entity").to("olingo4://delete/" + PEOPLE + "('lewisblack')");

                // test route for batch
                from("direct:batch").to("olingo4://batch");

                from("direct:read-etag").to("olingo4://read/Airlines('AA')").to("mock:check-etag-header");

                from("direct:delete-with-etag").to("olingo4://delete/Airlines('AA')");

                from("direct:read-people-nofilterseen").to("olingo4://read/" + PEOPLE).to("mock:producer-noalreadyseen");

                from("direct:read-people-filterseen").to("olingo4://read/" + PEOPLE + "?filterAlreadySeen=true").to("mock:producer-alreadyseen");

                // test routes action's
                from("direct:unbound-action-ResetDataSource").to("olingo4://action/ResetDataSource");

                from("direct:bound-action-people").to("olingo4://action/" + TEST_PEOPLE + "/Microsoft.OData.Service.Sample.TrippinInMemory.Models.ShareTrip");
            }
        };
    }
}
