/*******************************************************************************
 * 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.olingo.odata2.core.batch;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;

import org.apache.olingo.odata2.api.batch.BatchException;
import org.apache.olingo.odata2.api.batch.BatchRequestPart;
import org.apache.olingo.odata2.api.commons.HttpHeaders;
import org.apache.olingo.odata2.api.commons.ODataHttpMethod;
import org.apache.olingo.odata2.api.ep.EntityProviderBatchProperties;
import org.apache.olingo.odata2.api.processor.ODataRequest;
import org.apache.olingo.odata2.core.ODataPathSegmentImpl;
import org.apache.olingo.odata2.core.PathInfoImpl;
import org.apache.olingo.odata2.core.batch.v2.BatchParser;
import org.apache.olingo.odata2.testutil.helper.StringHelper;
import org.junit.BeforeClass;
import org.junit.Test;

public class BatchRequestParserTest {

  private static final String CRLF = "\r\n";
  private static final String CONTENT_ID_REFERENCE = "NewEmployee";
  private static final String PUT_MIME_HEADER_CONTENT_ID = "BBB_MIMEPART1";
  private static final String PUT_REQUEST_HEADER_CONTENT_ID = "BBB_REQUEST1";
  private static final String SERVICE_ROOT = "http://localhost/odata/";
  private static EntityProviderBatchProperties batchProperties;
  private static final String BOUNDARY = "batch_8194-cf13-1f56";
  private static final String contentType = "multipart/mixed;boundary=" + BOUNDARY;
  private static final String MIME_HEADERS = "Content-Type: application/http" + CRLF
      + "Content-Transfer-Encoding: binary" + CRLF;
  private static final String GET_REQUEST = MIME_HEADERS + CRLF
      + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
      + CRLF
      + CRLF;

  @BeforeClass
  public static void setProperties() throws URISyntaxException {
    PathInfoImpl pathInfo = new PathInfoImpl();
    pathInfo.setServiceRoot(new URI(SERVICE_ROOT));
    batchProperties = EntityProviderBatchProperties.init().pathInfo(pathInfo).build();
  }

  @Test
  public void test() throws IOException, BatchException, URISyntaxException {
    String fileName = "/batchWithPost.batch";
    InputStream in = ClassLoader.class.getResourceAsStream(fileName);
    if (in == null) {
      throw new IOException("Requested file '" + fileName + "' was not found.");
    }
    
    in = StringHelper.toStream(in).asStreamWithLineSeparation(CRLF);

    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in);
    assertNotNull(batchRequestParts);
    assertEquals(false, batchRequestParts.isEmpty());
    for (BatchRequestPart object : batchRequestParts) {
      if (!object.isChangeSet()) {
        assertEquals(1, object.getRequests().size());
        ODataRequest retrieveRequest = object.getRequests().get(0);
        assertEquals(ODataHttpMethod.GET, retrieveRequest.getMethod());
        if (!retrieveRequest.getAcceptableLanguages().isEmpty()) {
          assertEquals(3, retrieveRequest.getAcceptableLanguages().size());
        }
        assertEquals(new URI(SERVICE_ROOT), retrieveRequest.getPathInfo().getServiceRoot());
        ODataPathSegmentImpl pathSegment = new ODataPathSegmentImpl("Employees('2')", null);
        assertEquals(pathSegment.getPath(), retrieveRequest.getPathInfo().getODataSegments().get(0).getPath());
        if (retrieveRequest.getQueryParameters().get("$format") != null) {
          assertEquals("json", retrieveRequest.getQueryParameters().get("$format"));
        }
        assertEquals(SERVICE_ROOT + "Employees('2')/EmployeeName?$format=json", retrieveRequest.getPathInfo()
            .getRequestUri().toASCIIString());
      } else {
        List<ODataRequest> requests = object.getRequests();
        for (ODataRequest request : requests) {

          assertEquals(ODataHttpMethod.PUT, request.getMethod());
          assertEquals("100000", request.getRequestHeaderValue(HttpHeaders.CONTENT_LENGTH.toLowerCase()));
          assertEquals("application/json;odata=verbose", request.getContentType());
          assertEquals(3, request.getAcceptHeaders().size());
          assertNotNull(request.getAcceptableLanguages());
          assertTrue(request.getAcceptableLanguages().isEmpty());
          assertEquals("*/*", request.getAcceptHeaders().get(2));
          assertEquals("application/atomsvc+xml", request.getAcceptHeaders().get(0));
          assertEquals(new URI(SERVICE_ROOT + "Employees('2')/EmployeeName").toASCIIString(), request.getPathInfo()
              .getRequestUri().toASCIIString());

          ODataPathSegmentImpl pathSegment = new ODataPathSegmentImpl("Employees('2')", null);
          assertEquals(pathSegment.getPath(), request.getPathInfo().getODataSegments().get(0).getPath());
          ODataPathSegmentImpl pathSegment2 = new ODataPathSegmentImpl("EmployeeName", null);
          assertEquals(pathSegment2.getPath(), request.getPathInfo().getODataSegments().get(1).getPath());

        }
      }
    }
  }

  @Test
  public void testImageInContent() throws IOException, BatchException, URISyntaxException {
    String fileName = "/batchWithContent.batch";
    InputStream contentInputStream = ClassLoader.class.getResourceAsStream(fileName);
    if (contentInputStream == null) {
      throw new IOException("Requested file '" + fileName + "' was not found.");
    }
    String content = StringHelper.inputStreamToString(contentInputStream);
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET Employees?$filter=Age%20gt%2040 HTTP/1.1" + CRLF
        + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF
        + "MaxDataServiceVersion: 2.0" + CRLF
        + CRLF
        + CRLF
        + "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + "content-type:     Application/http" + CRLF
        + "content-transfer-encoding: Binary" + CRLF
        + "Content-ID: 1" + CRLF
        + CRLF
        + "POST Employees HTTP/1.1" + CRLF
        + "Content-length: 100000" + CRLF
        + "Content-type: application/octet-stream" + CRLF
        + CRLF
        + content + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + "--" + BOUNDARY + "--";
    List<BatchRequestPart> BatchRequestParts = parse(batch);
    for (BatchRequestPart object : BatchRequestParts) {
      if (!object.isChangeSet()) {
        assertEquals(1, object.getRequests().size());
        ODataRequest retrieveRequest = object.getRequests().get(0);
        assertEquals(ODataHttpMethod.GET, retrieveRequest.getMethod());
        assertEquals("Age gt 40", retrieveRequest.getQueryParameters().get("$filter"));
        assertEquals(new URI("http://localhost/odata/Employees?$filter=Age%20gt%2040"), retrieveRequest.getPathInfo()
            .getRequestUri());
      } else {
        List<ODataRequest> requests = object.getRequests();
        for (ODataRequest request : requests) {
          assertEquals(ODataHttpMethod.POST, request.getMethod());
          assertEquals("100000", request.getRequestHeaderValue(HttpHeaders.CONTENT_LENGTH.toLowerCase()));
          assertEquals("1", request.getRequestHeaderValue(BatchHelper.MIME_HEADER_CONTENT_ID));
          assertEquals("application/octet-stream", request.getContentType());
          InputStream body = request.getBody();
          assertEquals(content, StringHelper.inputStreamToString(body));

        }

      }
    }
  }

  @Test
  public void testPostWithoutBody() throws IOException, BatchException, URISyntaxException {
    String fileName = "/batchWithContent.batch";
    InputStream contentInputStream = ClassLoader.class.getResourceAsStream(fileName);
    if (contentInputStream == null) {
      throw new IOException("Requested file '" + fileName + "' was not found.");
    }
    StringHelper.inputStreamToString(contentInputStream);
    String batch = CRLF
        + "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: changeRequest1" + CRLF
        + CRLF
        + "POST Employees('2') HTTP/1.1" + CRLF
        + "Content-Length: 100" + CRLF
        + "Content-Type: application/octet-stream" + CRLF
        + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    List<BatchRequestPart> batchRequestParts = parse(batch);
    for (BatchRequestPart object : batchRequestParts) {
      if (object.isChangeSet()) {
        List<ODataRequest> requests = object.getRequests();
        for (ODataRequest request : requests) {
          assertEquals(ODataHttpMethod.POST, request.getMethod());
          assertEquals("100", request.getRequestHeaderValue(HttpHeaders.CONTENT_LENGTH.toLowerCase()));
          assertEquals("application/octet-stream", request.getContentType());
          assertNotNull(request.getBody());
        }
      }
    }
  }

  @Test
  public void testBoundaryParameterWithQuotas() throws BatchException {
    String contentType = "multipart/mixed; boundary=\"batch_1.2+34:2j)0?\"";

    String batch = "--batch_1.2+34:2j)0?" + CRLF
        + GET_REQUEST
        + "--batch_1.2+34:2j)0?--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in);
    assertNotNull(batchRequestParts);
    assertEquals(false, batchRequestParts.isEmpty());
  }

  @Test(expected = BatchException.class)
  public void testBatchWithInvalidContentType() throws BatchException {
    String invalidContentType = "multipart;boundary=batch_1740-bb84-2f7f";

    String batch = "--batch_1740-bb84-2f7f" + CRLF
        + GET_REQUEST
        + "--batch_1740-bb84-2f7f--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(invalidContentType, batchProperties, true);
    parser.parseBatchRequest(in);
  }

  @Test(expected = BatchException.class)
  public void testBatchWithoutBoundaryParameter() throws BatchException {
    String invalidContentType = "multipart/mixed";
    String batch = "--batch_1740-bb84-2f7f" + CRLF
        + GET_REQUEST
        + "--batch_1740-bb84-2f7f--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(invalidContentType, batchProperties, true);
    parser.parseBatchRequest(in);
  }

  @Test(expected = BatchException.class)
  public void testBoundaryParameterWithoutQuota() throws BatchException {
    String invalidContentType = "multipart/mixed;boundary=batch_1740-bb:84-2f7f";
    String batch = "--batch_1740-bb:84-2f7f" + CRLF
        + GET_REQUEST
        + "--batch_1740-bb:84-2f7f--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(invalidContentType, batchProperties, true);
    parser.parseBatchRequest(in);
  }

  @Test(expected = BatchException.class)
  public void testWrongBoundaryString() throws BatchException {
    String batch = "--batch_8194-cf13-1f5" + CRLF
        + GET_REQUEST
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testMissingHttpVersion() throws BatchException {
    String batch = ""
        + "--" + BOUNDARY + CRLF
        + "Content-Type: application/http" + CRLF
        + "Content-Transfer-Encoding:binary" + CRLF
        + CRLF
        + "GET Employees?$format=json" + CRLF
        + "Host: localhost:8080" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";

    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testMissingHttpVersion2() throws BatchException {
    String batch = ""
        + "--" + BOUNDARY + CRLF
        + "Content-Type: application/http" + CRLF
        + "Content-Transfer-Encoding:binary" + CRLF
        + CRLF
        + "GET Employees?$format=json " + CRLF
        + "Host: localhost:8080" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";

    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testMissingHttpVersion3() throws BatchException {
    String batch = ""
        + "--" + BOUNDARY + CRLF
        + "Content-Type: application/http" + CRLF
        + "Content-Transfer-Encoding:binary" + CRLF
        + CRLF
        + "GET Employees?$format=json SMTP:3.1" + CRLF
        + "Host: localhost:8080" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";

    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testBoundaryWithoutHyphen() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + GET_REQUEST
        + BOUNDARY + CRLF
        + GET_REQUEST
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testNoBoundaryString() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + GET_REQUEST
        // + no boundary string
        + GET_REQUEST
        + "--" + BOUNDARY + "--";

    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testBatchBoundaryEqualsChangeSetBoundary() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed;boundary=batch_8194-cf13-1f56" + CRLF
        + CRLF
        + "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "PUT Employees('2')/EmployeeName HTTP/1.1" + CRLF
        + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "MaxDataServiceVersion: 2.0" + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Frederic Fall MODIFIED\"}" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test
  public void testContentTypeCharset() throws BatchException {
    final String contentType = "multipart/mixed; charset=UTF-8;boundary=" + BOUNDARY;
    final String batch = "--" + BOUNDARY + CRLF
        + GET_REQUEST
        + "--" + BOUNDARY + "--";
    final BatchParser parser = new BatchParser(contentType, batchProperties, true);
    final List<BatchRequestPart> parts = parser.parseBatchRequest(new ByteArrayInputStream(batch.getBytes()));

    assertEquals(1, parts.size());
  }

  @Test
  public void testContentTypeCharsetWrongBoundaryAtEnd() throws BatchException {
    final String contentType = "multipart/mixed; charset=UTF-8;boundary=" + BOUNDARY + ";boundary=wrong_boundary";
    final String batch = "--" + BOUNDARY + CRLF
        + GET_REQUEST
        + "--" + BOUNDARY + "--";
    final BatchParser parser = new BatchParser(contentType, batchProperties, true);
    final List<BatchRequestPart> parts = parser.parseBatchRequest(new ByteArrayInputStream(batch.getBytes()));

    assertEquals(1, parts.size());
  }

  @Test(expected = BatchException.class)
  public void testContentTypeCharsetWrongBoundaryAtBeginning() throws BatchException {
    final String contentType = "multipart/mixed; charset=UTF-8;boundary=wrong_boundary;boundary=" + BOUNDARY;
    final String batch = "--" + BOUNDARY + CRLF
        + GET_REQUEST
        + "--" + BOUNDARY + "--";
    final BatchParser parser = new BatchParser(contentType, batchProperties, true);
    final List<BatchRequestPart> parts = parser.parseBatchRequest(new ByteArrayInputStream(batch.getBytes()));

    assertEquals(1, parts.size());
  }

  @Test(expected = BatchException.class)
  public void testNoContentType() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Transfer-Encoding: binary" + CRLF
        + CRLF
        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testMimeHeaderContentType() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: text/plain" + CRLF
        + "Content-Transfer-Encoding: binary" + CRLF
        + CRLF
        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testMimeHeaderEncoding() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: application/http" + CRLF
        + "Content-Transfer-Encoding: 8bit" + CRLF
        + CRLF
        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testGetRequestMissingCRLF() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + "Content-ID: 1" + CRLF
        + CRLF
        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
        // + CRLF // Belongs to the GET request
        + CRLF // Belongs to the
        + "--" + BOUNDARY + "--";

    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testInvalidMethodForBatch() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "POST Employees('1')/EmployeeName HTTP/1.1" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testNoBoundaryFound() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "POST Employees('1')/EmployeeName HTTP/1.1" + CRLF
        + CRLF;
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testBadRequest() throws BatchException {
    String batch = "This is a bad request. There is no syntax and also no semantic";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testNoMethod() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + /* GET */"Employees('1')/EmployeeName HTTP/1.1" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testInvalidMethodForChangeset() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET Employees('2')/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "MaxDataServiceVersion: 2.0" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testInvalidChangeSetBoundary() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94d"/* +"d" */+ CRLF
        + MIME_HEADERS
        + CRLF
        + "POST Employees('2') HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "MaxDataServiceVersion: 2.0" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testNestedChangeset() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + CRLF
        + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd2" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd2" + CRLF
        + MIME_HEADERS
        + CRLF
        + "POST Employees('2') HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "MaxDataServiceVersion: 2.0" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parse(batch);
  }

  @Test(expected = BatchException.class)
  public void testMissingContentTransferEncoding() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + "Content-Type: application/http" + CRLF
        // + "Content-Transfer-Encoding: binary" + CRLF
        + CRLF
        + "POST Employees('2') HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "MaxDataServiceVersion: 2.0" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testMissingContentType() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed;boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        // + "Content-Type: application/http" + CRLF
        + "Content-Transfer-Encoding: binary" + CRLF
        + CRLF
        + "POST Employees('2') HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "MaxDataServiceVersion: 2.0" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testNoCloseDelimiter() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + GET_REQUEST;
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testNoCloseDelimiter2() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF;
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testInvalidUri() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET http://localhost/aa/odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF
        + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test
  public void uriWithAbsolutePath() throws Exception {
    final String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET /odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF
        + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    final List<BatchRequestPart> batchRequestParts = parse(batch);
    assertEquals(1, batchRequestParts.size());
    assertEquals(1, batchRequestParts.get(0).getRequests().size());
    final ODataRequest request = batchRequestParts.get(0).getRequests().get(0);
    assertEquals(ODataHttpMethod.GET, request.getMethod());
  }

  @Test(expected = BatchException.class)
  public void uriWithWrongAbsolutePath() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET /wrong/Employees('1')/EmployeeName HTTP/1.1" + CRLF
        + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void wrongHost() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET /odata/Employees('1')/EmployeeName HTTP/1.1" + CRLF
        + "Host: wrongHost" + CRLF
        + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    parseInvalidBatchBody(batch);
  }

  @Test(expected = BatchException.class)
  public void testNoCloseDelimiter3() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + GET_REQUEST + "--" + BOUNDARY + "-";
    parseInvalidBatchBody(batch);
  }

  @Test
  public void testAcceptHeaders() throws BatchException, URISyntaxException {
    String batch = "--" + BOUNDARY + CRLF
            + MIME_HEADERS
            + CRLF
            + "GET Employees('2')/EmployeeName HTTP/1.1"
            + CRLF
            + "Content-Length: 100000"
            + CRLF
            + "Content-Type: application/json;odata=verbose"
            + CRLF
            + "Accept: application/xml;q=0.3, application/atomsvc+xml;q=0.8, " +
            "application/json;odata=verbose;q=0.5, */*;q=0.001"
            + CRLF
            + CRLF
            + CRLF
            + "--" + BOUNDARY + "--";
    List<BatchRequestPart> batchRequestParts = parse(batch);
    for (BatchRequestPart multipart : batchRequestParts) {
      if (!multipart.isChangeSet()) {
        assertEquals(1, multipart.getRequests().size());
        ODataRequest retrieveRequest = multipart.getRequests().get(0);
        assertEquals(ODataHttpMethod.GET, retrieveRequest.getMethod());
        assertNotNull(retrieveRequest.getAcceptHeaders());
        assertEquals(4, retrieveRequest.getAcceptHeaders().size());
        assertEquals("application/atomsvc+xml", retrieveRequest.getAcceptHeaders().get(0));
        assertEquals("*/*", retrieveRequest.getAcceptHeaders().get(3));
      }

    }
  }

  @Test
  public void testAcceptHeaders2() throws BatchException, URISyntaxException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET Employees('2')/EmployeeName HTTP/1.1" + CRLF
        + "Content-Length: 100000" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Accept: */*;q=0.5, application/json;odata=verbose;q=1.0,application/atom+xml" + CRLF
        + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    List<BatchRequestPart> batchRequestParts = parse(batch);
    for (BatchRequestPart multipart : batchRequestParts) {
      if (!multipart.isChangeSet()) {
        assertEquals(1, multipart.getRequests().size());
        ODataRequest retrieveRequest = multipart.getRequests().get(0);
        assertEquals(ODataHttpMethod.GET, retrieveRequest.getMethod());
        assertNotNull(retrieveRequest.getAcceptHeaders());
        assertEquals(3, retrieveRequest.getAcceptHeaders().size());
        assertEquals("application/json;odata=verbose", retrieveRequest.getAcceptHeaders().get(0));
        assertEquals("application/atom+xml", retrieveRequest.getAcceptHeaders().get(1));
        assertEquals("*/*", retrieveRequest.getAcceptHeaders().get(2));
      }

    }
  }

  @Test
  public void testAcceptHeaders3() throws BatchException, URISyntaxException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET Employees('2')/EmployeeName HTTP/1.1" + CRLF
        + "Content-Length: 100000" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF
        + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    List<BatchRequestPart> batchRequestParts = parse(batch);
    for (BatchRequestPart multipart : batchRequestParts) {
      if (!multipart.isChangeSet()) {
        assertEquals(1, multipart.getRequests().size());
        ODataRequest retrieveRequest = multipart.getRequests().get(0);
        assertEquals(ODataHttpMethod.GET, retrieveRequest.getMethod());
        assertNotNull(retrieveRequest.getAcceptHeaders());
        assertEquals(4, retrieveRequest.getAcceptHeaders().size());

        assertEquals("application/atom+xml", retrieveRequest.getAcceptHeaders().get(0));
        assertEquals("application/atomsvc+xml", retrieveRequest.getAcceptHeaders().get(1));

        assertEquals("application/xml", retrieveRequest.getAcceptHeaders().get(2));
      }

    }
  }

  @Test
  public void testNegativeContentLengthChangeSet() throws BatchException, IOException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: " + PUT_MIME_HEADER_CONTENT_ID + CRLF
        + "Content-Length: -2" + CRLF
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Content-Id:" + PUT_REQUEST_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    parser.parseBatchRequest(in);
  }

  @Test(expected = BatchException.class)
  public void testNegativeContentLengthRequest() throws BatchException, IOException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: " + PUT_MIME_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Content-Id:" + PUT_REQUEST_HEADER_CONTENT_ID + CRLF
        + "Content-Length: -2" + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    parser.parseBatchRequest(in);
  }

  @Test
  public void testContentLengthGreatherThanBodyLength() throws BatchException, IOException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: " + PUT_MIME_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Content-Id:" + PUT_REQUEST_HEADER_CONTENT_ID + CRLF
        + "Content-Length: 100000" + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in);
    assertNotNull(batchRequestParts);
    for (BatchRequestPart multipart : batchRequestParts) {
      if (multipart.isChangeSet()) {
        assertEquals(1, multipart.getRequests().size());
        ODataRequest request = multipart.getRequests().get(0);
        assertEquals("{\"EmployeeName\":\"Peter Fall\"}", inputStreamToString(request.getBody()));
      }
    }
  }

  @Test
  public void testContentLengthSmallerThanBodyLength() throws BatchException, IOException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: " + PUT_MIME_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Content-Id:" + PUT_REQUEST_HEADER_CONTENT_ID + CRLF
        + "Content-Length: 10" + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in);
    assertNotNull(batchRequestParts);
    for (BatchRequestPart multipart : batchRequestParts) {
      if (multipart.isChangeSet()) {
        assertEquals(1, multipart.getRequests().size());
        ODataRequest request = multipart.getRequests().get(0);
        assertEquals("{\"Employee", inputStreamToString(request.getBody()));
      }
    }
  }

  @Test(expected = BatchException.class)
  public void testNonNumericContentLength() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: " + PUT_MIME_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Content-Id:" + PUT_REQUEST_HEADER_CONTENT_ID + CRLF
        + "Content-Length: 10abc" + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    parser.parseBatchRequest(in);
  }

  @Test
  public void testNonStrictParser() throws BatchException, IOException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed;boundary=changeset_8194-cf13-1f56" + CRLF
        + "--changeset_8194-cf13-1f56" + CRLF
        + MIME_HEADERS
        + "Content-ID: myRequest" + CRLF
        + "PUT Employees('2')/EmployeeName HTTP/1.1" + CRLF
        + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "MaxDataServiceVersion: 2.0" + CRLF
        + "{\"EmployeeName\":\"Frederic Fall MODIFIED\"}" + CRLF
        + "--changeset_8194-cf13-1f56--" + CRLF
        + "--" + BOUNDARY + "--";

    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, false);
    List<BatchRequestPart> requests = parser.parseBatchRequest(in);
    assertNotNull(requests);
    assertEquals(1, requests.size());

    BatchRequestPart part = requests.get(0);
    assertTrue(part.isChangeSet());
    assertNotNull(part.getRequests());
    assertEquals(1, part.getRequests().size());

    ODataRequest changeRequest = part.getRequests().get(0);
    assertEquals("{\"EmployeeName\":\"Frederic Fall MODIFIED\"}", inputStreamToString(changeRequest.getBody()));
    assertEquals("application/json;odata=verbose", changeRequest.getContentType());
    assertEquals(ODataHttpMethod.PUT, changeRequest.getMethod());
  }

  @Test(expected = BatchException.class)
  public void testNonStrictParserMoreCRLF() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed;boundary=changeset_8194-cf13-1f56" + CRLF
        + "--changeset_8194-cf13-1f56" + CRLF
        + MIME_HEADERS
        + CRLF
        + CRLF // Only one CRLF allowed
        + "PUT Employees('2')/EmployeeName HTTP/1.1" + CRLF
        + "Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "MaxDataServiceVersion: 2.0" + CRLF
        + "{\"EmployeeName\":\"Frederic Fall MODIFIED\"}" + CRLF
        + "--changeset_8194-cf13-1f56--" + CRLF
        + "--" + BOUNDARY + "--";

    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, false);
    parser.parseBatchRequest(in);
  }

  @Test
  public void testContentId() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET Employees HTTP/1.1" + CRLF
        + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF
        + "Content-Id: BBB" + CRLF
        + CRLF + CRLF
        + "--batch_8194-cf13-1f56" + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-Id: " + CONTENT_ID_REFERENCE + CRLF
        + CRLF
        + "POST Employees HTTP/1.1" + CRLF
        + "Content-type: application/octet-stream" + CRLF
        + CRLF
        + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: " + PUT_MIME_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Content-Id:" + PUT_REQUEST_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in);
    assertNotNull(batchRequestParts);
    for (BatchRequestPart multipart : batchRequestParts) {
      if (!multipart.isChangeSet()) {
        assertEquals(1, multipart.getRequests().size());
        ODataRequest retrieveRequest = multipart.getRequests().get(0);
        assertEquals("BBB", retrieveRequest.getRequestHeaderValue(BatchHelper.REQUEST_HEADER_CONTENT_ID.toLowerCase()));
      } else {
        for (ODataRequest request : multipart.getRequests()) {
          if (ODataHttpMethod.POST.equals(request.getMethod())) {
            assertEquals(CONTENT_ID_REFERENCE, request.getRequestHeaderValue(BatchHelper.MIME_HEADER_CONTENT_ID
                .toLowerCase()));
          } else if (ODataHttpMethod.PUT.equals(request.getMethod())) {
            assertEquals(PUT_MIME_HEADER_CONTENT_ID, request.getRequestHeaderValue(BatchHelper.MIME_HEADER_CONTENT_ID
                .toLowerCase()));
            assertEquals(PUT_REQUEST_HEADER_CONTENT_ID, request
                .getRequestHeaderValue(BatchHelper.REQUEST_HEADER_CONTENT_ID.toLowerCase()));
            assertNull(request.getPathInfo().getRequestUri());
            assertEquals("$" + CONTENT_ID_REFERENCE, request.getPathInfo().getODataSegments().get(0).getPath());
          }
        }
      }
    }
  }

  @Test
  public void testNoContentId() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET Employees HTTP/1.1" + CRLF
        + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF
        + CRLF + CRLF
        + "--batch_8194-cf13-1f56" + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + CRLF
        + "POST Employees HTTP/1.1" + CRLF
        + "Content-type: application/octet-stream" + CRLF
        + CRLF
        + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in); // No exception should be thrown
    assertNotNull(batchRequestParts);
  }

  @Test
  public void testPreamble() throws BatchException, IOException {
    String batch = ""
        + "This is a preamble and must be ignored" + CRLF
        + CRLF
        + CRLF
        + "----1242" + CRLF
        +  "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET Employees HTTP/1.1" + CRLF
        + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF
        + "Content-Id: BBB" + CRLF
        + CRLF
        + CRLF
        + "--batch_8194-cf13-1f56" + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "This is a preamble and must be ignored" + CRLF
        + CRLF
        + CRLF
        + "----1242" + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-Id: " + CONTENT_ID_REFERENCE + CRLF
        + CRLF
        + "POST Employees HTTP/1.1" + CRLF
        + "Content-type: application/octet-stream" + CRLF
        + CRLF
        + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: " + PUT_MIME_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Content-Id:" + PUT_REQUEST_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in);

    assertNotNull(batchRequestParts);
    assertEquals(2, batchRequestParts.size());

    BatchRequestPart getRequestPart = batchRequestParts.get(0);
    assertEquals(1, getRequestPart.getRequests().size());
    ODataRequest getRequest = getRequestPart.getRequests().get(0);
    assertEquals(ODataHttpMethod.GET, getRequest.getMethod());

    BatchRequestPart changeSetPart = batchRequestParts.get(1);
    assertEquals(2, changeSetPart.getRequests().size());
    assertEquals("/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA"
        + CRLF,
        inputStreamToString(changeSetPart.getRequests().get(0).getBody()));
    assertEquals("{\"EmployeeName\":\"Peter Fall\"}",
        inputStreamToString(changeSetPart.getRequests().get(1).getBody()));
  }

  @SuppressWarnings("unused")
  @Test
  public void testContentTypeCaseInsensitive() throws BatchException, IOException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: muLTiParT/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: " + PUT_MIME_HEADER_CONTENT_ID + CRLF
        + "Content-Length: -2" + CRLF
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Content-Id:" + PUT_REQUEST_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in);
  }

  @Test
  public void testContentTypeBoundaryCaseInsensitive() throws BatchException, IOException {
    String batch = "--" + BOUNDARY + CRLF
        + "Content-Type: multipart/mixed; bOunDaRy=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: " + PUT_MIME_HEADER_CONTENT_ID + CRLF
        + "Content-Length: -2" + CRLF
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Content-Id:" + PUT_REQUEST_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "--" + BOUNDARY + "--";
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in);

    assertNotNull(batchRequestParts);
    assertEquals(1, batchRequestParts.size());
    assertTrue(batchRequestParts.get(0).isChangeSet());
    assertEquals(1, batchRequestParts.get(0).getRequests().size());
  }

  @Test
  public void testEpilog() throws BatchException, IOException {
    String batch = "--" + BOUNDARY + CRLF
        + MIME_HEADERS
        + CRLF
        + "GET Employees HTTP/1.1" + CRLF
        + "accept: */*,application/atom+xml,application/atomsvc+xml,application/xml" + CRLF
        + "Content-Id: BBB" + CRLF
        + CRLF
        + CRLF
        + "--batch_8194-cf13-1f56" + CRLF
        + "Content-Type: multipart/mixed; boundary=changeset_f980-1cb6-94dd" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-Id: " + CONTENT_ID_REFERENCE + CRLF
        + CRLF
        + "POST Employees HTTP/1.1" + CRLF
        + "Content-type: application/octet-stream" + CRLF
        + CRLF
        + "/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA" + CRLF
        + CRLF
        + "--changeset_f980-1cb6-94dd" + CRLF
        + MIME_HEADERS
        + "Content-ID: " + PUT_MIME_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "PUT $" + CONTENT_ID_REFERENCE + "/EmployeeName HTTP/1.1" + CRLF
        + "Content-Type: application/json;odata=verbose" + CRLF
        + "Content-Id:" + PUT_REQUEST_HEADER_CONTENT_ID + CRLF
        + CRLF
        + "{\"EmployeeName\":\"Peter Fall\"}" + CRLF
        + "--changeset_f980-1cb6-94dd--" + CRLF
        + CRLF
        + "This is an epilog and must be ignored" + CRLF
        + CRLF
        + CRLF
        + "----1242" + CRLF
        + "--" + BOUNDARY + "--" + CRLF
        + "This is an epilog and must be ignored" + CRLF
        + CRLF
        + CRLF
        + "----1242";

    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in);

    assertNotNull(batchRequestParts);
    assertEquals(2, batchRequestParts.size());

    BatchRequestPart getRequestPart = batchRequestParts.get(0);
    assertEquals(1, getRequestPart.getRequests().size());
    ODataRequest getRequest = getRequestPart.getRequests().get(0);
    assertEquals(ODataHttpMethod.GET, getRequest.getMethod());

    BatchRequestPart changeSetPart = batchRequestParts.get(1);
    assertEquals(2, changeSetPart.getRequests().size());
    assertEquals("/9j/4AAQSkZJRgABAQEBLAEsAAD/4RM0RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAA"
        + CRLF,
        inputStreamToString(changeSetPart.getRequests().get(0).getBody()));
    assertEquals("{\"EmployeeName\":\"Peter Fall\"}",
        inputStreamToString(changeSetPart.getRequests().get(1).getBody()));
  }

  @Test
  public void testLargeBatch() throws BatchException, IOException {
    String fileName = "/batchLarge.batch";
    InputStream in = ClassLoader.class.getResourceAsStream(fileName);
    if (in == null) {
      throw new IOException("Requested file '" + fileName + "' was not found.");
    }
    in = StringHelper.toStream(in).asStreamWithLineSeparation(CRLF);
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    parser.parseBatchRequest(in);
  }

  @Test
  public void testNonStrictGetRequestWithMissingCRLF() throws BatchException {
    String batch = "--" + BOUNDARY + CRLF
            + MIME_HEADERS
            + "Content-ID: 1" + CRLF
            + CRLF
            + "GET Employees('1')/EmployeeName HTTP/1.1" + CRLF
            // + CRLF // Belongs to the GET request
            + CRLF // Belongs to the
            + "--" + BOUNDARY + "--";

    parse(batch, false);
  }

  private List<BatchRequestPart> parse(final String batch) throws BatchException {
    return parse(batch, true);
  }

  private List<BatchRequestPart> parse(final String batch, final boolean isStrict) throws BatchException {
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, isStrict);
    List<BatchRequestPart> batchRequestParts = parser.parseBatchRequest(in);
    assertNotNull(batchRequestParts);
    assertEquals(false, batchRequestParts.isEmpty());
    return batchRequestParts;
  }

  private void parseInvalidBatchBody(final String batch) throws BatchException {
    InputStream in = new ByteArrayInputStream(batch.getBytes());
    BatchParser parser = new BatchParser(contentType, batchProperties, true);
    parser.parseBatchRequest(in);
  }

  private String inputStreamToString(final InputStream in) throws IOException {
    int input;
    final StringBuilder builder = new StringBuilder();

    while ((input = in.read()) != -1) {
      builder.append((char) input);
    }

    return builder.toString();
  }
}
