content: /*
   * 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.salesforce;
  
  import java.io.IOException;
  import java.util.Arrays;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.stream.Collectors;
  
  import org.apache.camel.builder.RouteBuilder;
  import org.apache.camel.component.salesforce.api.dto.AbstractQueryRecordsBase;
  import org.apache.camel.component.salesforce.api.dto.UpsertSObjectResult;
  import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch;
  import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch.Method;
  import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResponse;
  import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResult;
  import org.apache.camel.component.salesforce.api.utils.Version;
  import org.apache.camel.component.salesforce.dto.generated.Account;
  import org.apache.camel.test.junit5.params.Parameter;
  import org.apache.camel.test.junit5.params.Parameterized;
  import org.apache.camel.test.junit5.params.Parameters;
  import org.apache.camel.test.junit5.params.Test;
  import org.junit.jupiter.api.BeforeEach;
  
  import static org.junit.jupiter.api.Assertions.assertEquals;
  import static org.junit.jupiter.api.Assertions.assertFalse;
  import static org.junit.jupiter.api.Assertions.assertNotNull;
  import static org.junit.jupiter.api.Assertions.assertTrue;
  
  @Parameterized
  public class CompositeApiBatchManualIT extends AbstractSalesforceTestBase {
  
      public static class Accounts extends AbstractQueryRecordsBase<Account> {
      }
  
      private static final Set<String> VERSIONS
              = new HashSet<>(Arrays.asList(SalesforceEndpointConfig.DEFAULT_VERSION, "34.0"));
  
      private static final String ACCOUNT_EXTERNAL_ID = "CompositeAPIBatch";
  
      @Parameter
      protected String format;
  
      @Parameter(1)
      protected String version;
  
      private String accountId;
  
      @BeforeEach
      public void setupRecords() throws InterruptedException {
          if (accountId != null) {
              return;
          }
  
          final Account account = new Account();
          account.setName("Composite API Batch");
          account.setExternal_Id__c(ACCOUNT_EXTERNAL_ID);
  
          final UpsertSObjectResult result = template.requestBody(
                  "salesforce:upsertSObject?sObjectIdName=External_Id__c", account, UpsertSObjectResult.class);
          accountId = result.getId();
  
          if (result.getCreated()) {
              // Give the indexer some time to index this account
              Thread.sleep(2000);
          }
      }
  
      @Test
      public void shouldSubmitBatchUsingCompositeApi() {
          final SObjectBatch batch = new SObjectBatch(version);
  
          final Account updates = new Account();
          updates.setName("NewName");
          batch.addUpdate("Account", accountId, updates);
  
          final Account newAccount = new Account();
          newAccount.setName("Account created from Composite batch API");
          batch.addCreate(newAccount);
  
          batch.addGet("Account", accountId, "Name", "BillingPostalCode");
  
          batch.addDelete("Account", accountId);
  
          final SObjectBatchResponse response = template.requestBody(batchUri(), batch, SObjectBatchResponse.class);
  
          assertNotNull(response, "Response should be provided");
  
          assertFalse(response.hasErrors());
      }
  
      @Test
      public void shouldSupportGenericBatchRequests() {
          final SObjectBatch batch = new SObjectBatch(version);
  
          batch.addGeneric(Method.GET, "/sobjects/Account/" + accountId);
  
          testBatch(batch);
      }
  
      /**
       * The XML format fails, as Salesforce API wrongly includes whitespaces inside tag names. E.g. <Ant Migration Tool>
       * https://www.w3.org/TR/2008/REC-xml-20081126/#NT-NameChar
       */
      @Test
      public void shouldSupportLimits() {
          final SObjectBatch batch = new SObjectBatch(version);
  
          batch.addLimits();
  
          final SObjectBatchResponse response = testBatch(batch);
  
          final List<SObjectBatchResult> results = response.getResults();
          final SObjectBatchResult batchResult = results.get(0);
  
          @SuppressWarnings("unchecked")
          final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
  
          // JSON and XML structure are different, XML has `LimitsSnapshot` node,
          // JSON does not
          @SuppressWarnings("unchecked")
          final Map<String, Object> limits = (Map<String, Object>) result.getOrDefault("LimitsSnapshot", result);
  
          @SuppressWarnings("unchecked")
          final Map<String, String> apiRequests = (Map<String, String>) limits.get("DailyApiRequests");
  
          // for JSON value will be Integer, for XML (no type information) it will
          // be String
          // This number can be different per org, and future releases,
          // so let's just make sure it's greater than zero
          assertTrue(Integer.valueOf(String.valueOf(apiRequests.get("Max"))) > 0);
      }
  
      @Test
      public void shouldSupportObjectCreation() {
          final SObjectBatch batch = new SObjectBatch(version);
  
          final Account newAccount = new Account();
          newAccount.setName("Account created from Composite batch API");
          batch.addCreate(newAccount);
  
          final SObjectBatchResponse response = testBatch(batch);
  
          final List<SObjectBatchResult> results = response.getResults();
  
          final SObjectBatchResult batchResult = results.get(0);
  
          @SuppressWarnings("unchecked")
          final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
  
          // JSON and XML structure are different, XML has `Result` node, JSON
          // does not
          @SuppressWarnings("unchecked")
          final Map<String, Object> creationOutcome = (Map<String, Object>) result.getOrDefault("Result", result);
  
          assertNotNull(creationOutcome.get("id"));
      }
  
      @Test
      public void shouldSupportObjectDeletion() {
          final SObjectBatch batch = new SObjectBatch(version);
  
          batch.addDelete("Account", accountId);
  
          testBatch(batch);
      }
  
      @Test
      public void shouldSupportObjectRetrieval() {
          final SObjectBatch batch = new SObjectBatch(version);
  
          batch.addGet("Account", accountId, "Name");
  
          final SObjectBatchResponse response = testBatch(batch);
  
          final List<SObjectBatchResult> results = response.getResults();
          final SObjectBatchResult batchResult = results.get(0);
  
          @SuppressWarnings("unchecked")
          final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
  
          // JSON and XML structure are different, XML has `Account` node, JSON
          // does not
          @SuppressWarnings("unchecked")
          final Map<String, String> data = (Map<String, String>) result.getOrDefault("Account", result);
  
          assertEquals("Composite API Batch", data.get("Name"));
      }
  
      @Test
      public void shouldSupportObjectUpdates() {
          final SObjectBatch batch = new SObjectBatch(version);
  
          final Account updates = new Account();
          updates.setName("NewName");
          updates.setAccountNumber("AC12345");
          batch.addUpdate("Account", accountId, updates);
  
          testBatch(batch);
      }
  
      @Test
      public void shouldSupportQuery() {
          final SObjectBatch batch = new SObjectBatch(version);
  
          batch.addQuery("SELECT Id, Name FROM Account");
  
          final SObjectBatchResponse response = testBatch(batch);
  
          final List<SObjectBatchResult> results = response.getResults();
          final SObjectBatchResult batchResult = results.get(0);
  
          @SuppressWarnings("unchecked")
          final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
  
          // JSON and XML structure are different, XML has `QueryResult` node,
          // JSON does not
          @SuppressWarnings("unchecked")
          final Map<String, String> data = (Map<String, String>) result.getOrDefault("QueryResult", result);
  
          assertNotNull(data.get("totalSize"));
      }
  
      @Test
      public void shouldSupportQueryAll() {
          final SObjectBatch batch = new SObjectBatch(version);
  
          batch.addQueryAll("SELECT Id, Name FROM Account");
  
          final SObjectBatchResponse response = testBatch(batch);
  
          final List<SObjectBatchResult> results = response.getResults();
          final SObjectBatchResult batchResult = results.get(0);
  
          @SuppressWarnings("unchecked")
          final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
  
          // JSON and XML structure are different, XML has `QueryResult` node,
          // JSON does not
          @SuppressWarnings("unchecked")
          final Map<String, String> data = (Map<String, String>) result.getOrDefault("QueryResult", result);
  
          assertNotNull(data.get("totalSize"));
      }
  
      @Test
      public void shouldSupportRelatedObjectRetrieval() throws IOException {
          if (Version.create(version).compareTo(Version.create("36.0")) < 0) {
              return;
          }
  
          final SObjectBatch batch = new SObjectBatch("36.0");
  
          batch.addGetRelated("Account", accountId, "CreatedBy");
  
          final SObjectBatchResponse response = testBatch(batch);
  
          final List<SObjectBatchResult> results = response.getResults();
          final SObjectBatchResult batchResult = results.get(0);
  
          @SuppressWarnings("unchecked")
          final Map<String, Object> result = (Map<String, Object>) batchResult.getResult();
  
          // JSON and XML structure are different, XML has `User` node, JSON does
          // not
          @SuppressWarnings("unchecked")
          final Map<String, String> data = (Map<String, String>) result.getOrDefault("User", result);
  
          final SalesforceLoginConfig loginConfig = LoginConfigHelper.getLoginConfig();
  
          assertEquals(loginConfig.getUserName(), data.get("Username"));
      }
  
      @Test
      public void shouldSupportSearch() {
          final SObjectBatch batch = new SObjectBatch(version);
  
          // we cannot rely on search returning the `Composite API Batch` account
          // as the search indexer runs
          // asynchronously to object creation, so that account might not be
          // indexed at this time, so we search for
          // `United` Account that should be created with developer instance
          batch.addSearch("FIND {Composite API Batch} IN Name Fields RETURNING Account (Name)");
  
          final SObjectBatchResponse response = testBatch(batch);
  
          final List<SObjectBatchResult> results = response.getResults();
          final SObjectBatchResult batchResult = results.get(0);
  
          final Object firstBatchResult = batchResult.getResult();
  
          final Object searchResult;
          if (firstBatchResult instanceof Map) {
              // the JSON and XML responses differ, XML has a root node which can
              // be either SearchResults or
              // SearchResultWithMetadata
              // furthermore version 37.0 search results are no longer array, but
              // dictionary of {
              // "searchRecords": [<array>] } and the XML output changed to
              // <SearchResultWithMetadata><searchRecords>, so
              // we have:
              // @formatter:off
              // | version | format | response syntax |
              // | 34 | JSON | {attributes={type=Account... |
              // | 34 | XML | {SearchResults={attributes={type=Account... |
              // | 37 | JSON | {searchRecords=[{attributes={type=Account... |
              // | 37 | XML |
              // {SearchResultWithMetadata={searchRecords={attributes={type=Account...
              // |
              // @formatter:on
              @SuppressWarnings("unchecked")
              final Map<String, Object> tmp = (Map<String, Object>) firstBatchResult;
  
              @SuppressWarnings("unchecked")
              final Map<String, Object> nested = (Map<String, Object>) tmp.getOrDefault("SearchResultWithMetadata", tmp);
  
              // JSON and XML structure are different, XML has `SearchResults`
              // node, JSON does not
              searchResult = nested.getOrDefault("searchRecords", nested.getOrDefault("SearchResults", nested));
          } else {
              searchResult = firstBatchResult;
          }
  
          final Map<String, Object> result;
          if (searchResult instanceof List) {
              @SuppressWarnings("unchecked")
              final Map<String, Object> tmp = (Map<String, Object>) ((List) searchResult).get(0);
              result = tmp;
          } else {
              @SuppressWarnings("unchecked")
              final Map<String, Object> tmp = (Map<String, Object>) searchResult;
              result = tmp;
          }
  
          assertNotNull(result.get("Name"));
      }
  
      @Override
      protected RouteBuilder doCreateRouteBuilder() throws Exception {
          return new RouteBuilder() {
              @Override
              public void configure() throws Exception {
                  from("direct:deleteBatchAccounts")
                          .to("salesforce:query?sObjectClass=" + Accounts.class.getName()
                              + "&sObjectQuery=SELECT Id FROM Account WHERE Name = 'Account created from Composite batch API'")
                          .split(simple("${body.records}")).setHeader("sObjectId", simple("${body.id}"))
                          .to("salesforce:deleteSObject?sObjectName=Account").end();
              }
          };
      }
  
      SObjectBatchResponse testBatch(final SObjectBatch batch) {
          final SObjectBatchResponse response = template.requestBody(batchUri(), batch, SObjectBatchResponse.class);
  
          assertNotNull(response, "Response should be provided");
  
          assertFalse(response.hasErrors(), "Received errors in: " + response);
  
          return response;
      }
  
      @Parameters(name = "format = {0}, version = {1}")
      public static Iterable<Object[]> formats() {
          return VERSIONS.stream().map(v -> new Object[] { "JSON", v }).collect(Collectors.toList());
      }
  
      private String batchUri() {
          return "salesforce:composite-batch?format=" + format;
      }
  }
  