/*
 * 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.dataformat.mime.multipart;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.util.ByteArrayDataSource;

import org.apache.camel.Exchange;
import org.apache.camel.attachment.Attachment;
import org.apache.camel.attachment.AttachmentMessage;
import org.apache.camel.attachment.DefaultAttachment;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.support.DefaultExchange;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.apache.camel.util.IOHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
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.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class MimeMultipartDataFormatTest extends CamelTestSupport {
    private Exchange exchange;
    private AttachmentMessage in;

    @Override
    @BeforeEach
    public void setUp() throws Exception {
        super.setUp();
        exchange = new DefaultExchange(context);
        in = exchange.getIn(AttachmentMessage.class);
    }

    @Test
    public void roundtripWithTextAttachments() throws IOException {
        String attContentType = "text/plain";
        String attText = "Attachment Text";
        String attFileName = "Attachment File Name";
        in.setBody("Body text");
        in.setHeader(Exchange.CONTENT_TYPE, "text/plain;charset=iso8859-1;other-parameter=true");
        in.setHeader(Exchange.CONTENT_ENCODING, "UTF8");
        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Description", "Sample Attachment Data");
        headers.put("X-AdditionalData", "additional data");
        addAttachment(attContentType, attText, attFileName, headers);
        Exchange result = template.send("direct:roundtrip", exchange);
        AttachmentMessage out = result.getMessage(AttachmentMessage.class);
        assertEquals("Body text", out.getBody(String.class));
        assertTrue(out.getHeader(Exchange.CONTENT_TYPE, String.class).startsWith("text/plain"));
        assertEquals("UTF8", out.getHeader(Exchange.CONTENT_ENCODING));
        assertTrue(out.hasAttachments());
        assertEquals(1, out.getAttachmentNames().size());
        assertTrue(out.getAttachmentNames().contains(attFileName));
        Attachment att = out.getAttachmentObject(attFileName);
        DataHandler dh = att.getDataHandler();
        assertNotNull(dh);
        assertEquals(attContentType, dh.getContentType());
        InputStream is = dh.getInputStream();
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        IOHelper.copyAndCloseInput(is, os);
        assertEquals(attText, new String(os.toByteArray()));
        assertEquals("Sample Attachment Data", att.getHeader("content-description"));
        assertEquals("additional data", att.getHeader("X-AdditionalData"));
    }

    @Test
    public void roundtripWithTextAttachmentsHeadersInline() throws IOException {
        String attContentType = "text/plain";
        String attText = "Attachment Text";
        String attFileName = "Attachment File Name";
        in.setBody("Body text");
        in.setHeader(Exchange.CONTENT_TYPE, "text/plain;charset=iso8859-1;other-parameter=true");
        in.setHeader(Exchange.CONTENT_ENCODING, "UTF8");
        addAttachment(attContentType, attText, attFileName);
        Exchange result = template.send("direct:roundtripinlineheaders", exchange);
        AttachmentMessage out = result.getMessage(AttachmentMessage.class);
        assertEquals("Body text", out.getBody(String.class));
        assertTrue(out.getHeader(Exchange.CONTENT_TYPE, String.class).startsWith("text/plain"));
        assertEquals("UTF8", out.getHeader(Exchange.CONTENT_ENCODING));
        assertTrue(out.hasAttachments());
        assertEquals(1, out.getAttachmentNames().size());
        assertTrue(out.getAttachmentNames().contains(attFileName));
        DataHandler dh = out.getAttachment(attFileName);
        assertNotNull(dh);
        assertEquals(attContentType, dh.getContentType());
        InputStream is = dh.getInputStream();
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        IOHelper.copyAndCloseInput(is, os);
        assertEquals(attText, new String(os.toByteArray()));
    }

    @Test
    @Disabled("Fails on CI servers and some platforms - maybe due locale or something")
    public void roundtripWithTextAttachmentsAndSpecialCharacters() throws IOException {
        String attContentType = "text/plain";
        String attText = "Attachment Text with special characters: \u00A9";
        String attFileName = "Attachment File Name with special characters: \u00A9";
        in.setBody("Body text with special characters: \u00A9");
        in.setHeader(Exchange.CONTENT_TYPE, "text/plain");
        in.setHeader(Exchange.CONTENT_ENCODING, "UTF8");
        addAttachment(attContentType, attText, attFileName);
        Exchange result = template.send("direct:roundtrip", exchange);
        AttachmentMessage out = result.getMessage(AttachmentMessage.class);
        assertEquals("Body text with special characters: \u00A9", out.getBody(String.class));
        assertTrue(out.getHeader(Exchange.CONTENT_TYPE, String.class).startsWith("text/plain"));
        assertEquals("UTF8", out.getHeader(Exchange.CONTENT_ENCODING));
        assertTrue(out.hasAttachments());
        assertEquals(1, out.getAttachmentNames().size());
        assertTrue(out.getAttachmentNames().contains(attFileName));
        DataHandler dh = out.getAttachment(attFileName);
        assertNotNull(dh);
        assertEquals(attContentType, dh.getContentType());
        InputStream is = dh.getInputStream();
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        IOHelper.copyAndCloseInput(is, os);
        assertEquals(attText, new String(os.toByteArray()));
    }

    @Test
    public void roundtripWithTextAttachmentsAndBinaryContent() throws IOException {
        String attContentType = "text/plain";
        String attText = "Attachment Text";
        String attFileName = "Attachment File Name";
        in.setBody("Body text");
        in.setHeader(Exchange.CONTENT_TYPE, "text/plain;charset=iso8859-1;other-parameter=true");
        addAttachment(attContentType, attText, attFileName);
        Exchange result = template.send("direct:roundtripbinarycontent", exchange);
        AttachmentMessage out = result.getMessage(AttachmentMessage.class);
        assertEquals("Body text", out.getBody(String.class));
        assertTrue(out.getHeader(Exchange.CONTENT_TYPE, String.class).startsWith("text/plain"));
        assertEquals("iso8859-1", out.getHeader(Exchange.CONTENT_ENCODING));
        assertTrue(out.hasAttachments());
        assertEquals(1, out.getAttachmentNames().size());
        assertTrue(out.getAttachmentNames().contains(attFileName));
        DataHandler dh = out.getAttachment(attFileName);
        assertNotNull(dh);
        assertEquals(attContentType, dh.getContentType());
        InputStream is = dh.getInputStream();
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        IOHelper.copyAndCloseInput(is, os);
        assertEquals(attText, new String(os.toByteArray()));
    }

    @Test
    public void roundtripWithBinaryAttachments() throws IOException {
        String attContentType = "application/binary";
        byte[] attText = { 0, 1, 2, 3, 4, 5, 6, 7 };
        String attFileName = "Attachment File Name";
        in.setBody("Body text");
        DataSource ds = new ByteArrayDataSource(attText, attContentType);
        in.addAttachment(attFileName, new DataHandler(ds));
        Exchange result = template.send("direct:roundtrip", exchange);
        AttachmentMessage out = result.getMessage(AttachmentMessage.class);
        assertEquals("Body text", out.getBody(String.class));
        assertTrue(out.hasAttachments());
        assertEquals(1, out.getAttachmentNames().size());
        assertTrue(out.getAttachmentNames().contains(attFileName));
        DataHandler dh = out.getAttachment(attFileName);
        assertNotNull(dh);
        assertEquals(attContentType, dh.getContentType());
        InputStream is = dh.getInputStream();
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        IOHelper.copyAndCloseInput(is, os);
        assertArrayEquals(attText, os.toByteArray());
    }

    @Test
    public void roundtripWithBinaryAttachmentsAndBinaryContent() throws IOException {
        String attContentType = "application/binary";
        byte[] attText = { 0, 1, 2, 3, 4, 5, 6, 7 };
        String attFileName = "Attachment File Name";
        in.setBody("Body text");
        DataSource ds = new ByteArrayDataSource(attText, attContentType);
        in.addAttachment(attFileName, new DataHandler(ds));
        Exchange result = template.send("direct:roundtripbinarycontent", exchange);
        AttachmentMessage out = result.getMessage(AttachmentMessage.class);
        assertEquals("Body text", out.getBody(String.class));
        assertTrue(out.hasAttachments());
        assertEquals(1, out.getAttachmentNames().size());
        assertTrue(out.getAttachmentNames().contains(attFileName));
        DataHandler dh = out.getAttachment(attFileName);
        assertNotNull(dh);
        assertEquals(attContentType, dh.getContentType());
        InputStream is = dh.getInputStream();
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        IOHelper.copyAndCloseInput(is, os);
        assertArrayEquals(attText, os.toByteArray());
    }

    @Test
    public void roundtripWithoutAttachments() {
        in.setBody("Body text");
        Exchange result = template.send("direct:roundtrip", exchange);
        AttachmentMessage out = result.getMessage(AttachmentMessage.class);
        assertEquals("Body text", out.getBody(String.class));
        assertFalse(out.hasAttachments());
    }

    @Test
    public void roundtripWithoutAttachmentsToMultipart() {
        in.setBody("Body text");
        Exchange result = template.send("direct:roundtripmultipart", exchange);
        AttachmentMessage out = result.getMessage(AttachmentMessage.class);
        assertEquals("Body text", out.getBody(String.class));
        assertFalse(out.hasAttachments());
    }

    @Test
    public void roundtripWithoutAttachmentsAndContentType() {
        in.setBody("Body text");
        in.setHeader("Content-Type", "text/plain");
        Exchange result = template.send("direct:roundtrip", exchange);
        AttachmentMessage out = result.getMessage(AttachmentMessage.class);
        assertEquals("Body text", out.getBody(String.class));
        assertFalse(out.hasAttachments());
    }

    @Test
    public void roundtripWithoutAttachmentsAndInvalidContentType() {
        in.setBody("Body text");
        in.setHeader("Content-Type", "text?plain");
        Exchange result = template.send("direct:roundtrip", exchange);
        AttachmentMessage out = result.getMessage(AttachmentMessage.class);
        assertEquals("Body text", out.getBody(String.class));
        assertFalse(out.hasAttachments());
    }

    @Test
    public void marhsalOnlyMixed() throws IOException {
        in.setBody("Body text");
        in.setHeader("Content-Type", "text/plain");
        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Description", "Sample Attachment Data");
        headers.put("X-AdditionalData", "additional data");
        addAttachment("application/octet-stream", "foobar", "attachment.bin", headers);
        Exchange result = template.send("direct:marshalonlymixed", exchange);
        assertTrue(result.getMessage().getHeader("Content-Type", String.class).startsWith("multipart/mixed"));
        String resultBody = result.getMessage().getBody(String.class);
        assertTrue(resultBody.contains("Content-Description: Sample Attachment Data"));
    }

    @Test
    public void marhsalOnlyRelated() throws IOException {
        in.setBody("Body text");
        in.setHeader("Content-Type", "text/plain");
        addAttachment("application/octet-stream", "foobar", "attachment.bin");
        Exchange result = template.send("direct:marshalonlyrelated", exchange);
        assertTrue(result.getMessage().getHeader("Content-Type", String.class).startsWith("multipart/related"));
    }

    @Test
    public void marhsalUnmarshalInlineHeaders() throws IOException {
        in.setBody("Body text");
        in.setHeader("Content-Type", "text/plain");
        in.setHeader("included", "must be included");
        in.setHeader("excluded", "should not be there");
        in.setHeader("x-foo", "any value");
        in.setHeader("x-bar", "also there");
        in.setHeader("xbar", "should not be there");
        addAttachment("application/octet-stream", "foobar", "attachment.bin");
        Exchange intermediate = template.send("direct:marshalonlyinlineheaders", exchange);
        String bodyStr = intermediate.getMessage().getBody(String.class);
        assertTrue(bodyStr.contains("must be included"));
        assertFalse(bodyStr.contains("should not be there"));
        assertTrue(bodyStr.contains("x-foo:"));
        assertTrue(bodyStr.contains("x-bar:"));
        assertFalse(bodyStr.contains("xbar"));
        intermediate.setIn(intermediate.getMessage());
        intermediate.setMessage(null);
        intermediate.getIn().removeHeaders(".*");
        intermediate.getIn().setHeader("included", "should be replaced");
        Exchange out = template.send("direct:unmarshalonlyinlineheaders", intermediate);
        assertEquals("Body text", out.getMessage().getBody(String.class));
        assertEquals("must be included", out.getMessage().getHeader("included"));
        assertNull(out.getMessage().getHeader("excluded"));
        assertEquals("any value", out.getMessage().getHeader("x-foo"));
        assertEquals("also there", out.getMessage().getHeader("x-bar"));
    }

    @Test
    public void unmarshalRelated() throws IOException {
        in.setBody(new File("src/test/resources/multipart-related.txt"));
        Attachment dh = unmarshalAndCheckAttachmentName("950120.aaCB@XIson.com");
        assertNotNull(dh);
        assertEquals("The fixed length records", dh.getHeader("Content-Description"));
        assertEquals("header value1,header value2", dh.getHeader("X-Additional-Header"));
        assertEquals(2, dh.getHeaderAsList("X-Additional-Header").size());
    }

    @Test
    public void unmarshalWithoutId() throws IOException {
        in.setBody(new File("src/test/resources/multipart-without-id.txt"));
        unmarshalAndCheckAttachmentName("@camel.apache.org");
    }

    @Test
    public void unmarshalNonMimeBody() {
        in.setBody("This is not a MIME-Multipart");
        Exchange out = template.send("direct:unmarshalonly", exchange);
        assertNotNull(out.getMessage());
        String bodyStr = out.getMessage().getBody(String.class);
        assertEquals("This is not a MIME-Multipart", bodyStr);
    }

    @Test
    public void unmarshalInlineHeadersNonMimeBody() {
        in.setBody("This is not a MIME-Multipart");
        Exchange out = template.send("direct:unmarshalonlyinlineheaders", exchange);
        assertNotNull(out.getMessage());
        String bodyStr = out.getMessage().getBody(String.class);
        assertEquals("This is not a MIME-Multipart", bodyStr);
    }

    /*
     * This test will only work of stream caching is enabled on the route. In order to find out whether the body
     * is a multipart or not the stream has to be read, and if the underlying data is a stream (but not a stream cache)
     * there is no way back
     */
    @Test
    public void unmarshalInlineHeadersNonMimeBodyStream() {
        in.setBody(new ByteArrayInputStream("This is not a MIME-Multipart".getBytes(StandardCharsets.UTF_8)));
        Exchange out = template.send("direct:unmarshalonlyinlineheaders", exchange);
        assertNotNull(out.getMessage());
        String bodyStr = out.getMessage().getBody(String.class);
        assertEquals("This is not a MIME-Multipart", bodyStr);
    }

    private Attachment unmarshalAndCheckAttachmentName(String matcher) throws IOException {
        Exchange intermediate = template.send("direct:unmarshalonlyinlineheaders", exchange);
        assertNotNull(intermediate.getMessage());
        String bodyStr = intermediate.getMessage().getBody(String.class);
        assertNotNull(bodyStr);
        assertTrue(bodyStr.startsWith("25"));
        assertEquals(1, intermediate.getMessage(AttachmentMessage.class).getAttachmentNames().size());
        assertTrue(intermediate.getMessage(AttachmentMessage.class).getAttachmentNames().iterator().next().contains(matcher));
        Attachment att = intermediate.getMessage(AttachmentMessage.class)
                .getAttachmentObject(intermediate.getMessage(AttachmentMessage.class).getAttachmentNames().iterator().next());
        DataHandler dh = att.getDataHandler();
        assertNotNull(dh);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        dh.writeTo(bos);
        String attachmentString = new String(bos.toByteArray(), StandardCharsets.UTF_8);
        assertTrue(attachmentString.startsWith("Old MacDonald had a farm"));
        return att;
    }

    private void addAttachment(String attContentType, String attText, String attFileName) throws IOException {
        addAttachment(attContentType, attText, attFileName, null);
    }

    private void addAttachment(String attContentType, String attText, String attFileName, Map<String, String> headers)
            throws IOException {
        DataSource ds = new ByteArrayDataSource(attText, attContentType);
        DefaultAttachment attachment = new DefaultAttachment(ds);
        if (headers != null) {
            for (String headerName : headers.keySet()) {
                attachment.addHeader(headerName, headers.get(headerName));
            }
        }
        in.addAttachmentObject(attFileName, attachment);
    }

    @Override
    protected RouteBuilder createRouteBuilder() {
        return new RouteBuilder() {
            @Override
            public void configure() {
                from("direct:roundtrip").marshal().mimeMultipart().to("log:mime?showHeaders=true").unmarshal().mimeMultipart();
                from("direct:roundtripmultipart").marshal().mimeMultipart(true, false, false).to("log:mime?showHeaders=true")
                        .unmarshal().mimeMultipart();
                from("direct:roundtripinlineheaders").marshal().mimeMultipart(false, true, false)
                        .to("log:mime?showHeaders=true").unmarshal().mimeMultipart(false, true, false);
                from("direct:roundtripbinarycontent").marshal().mimeMultipart(false, false, true)
                        .to("log:mime?showHeaders=true").to("dataformat:mime-multipart:unmarshal");
                from("direct:marshalonlyrelated").marshal().mimeMultipart("related");
                from("direct:marshalonlymixed").marshal().mimeMultipart();
                from("direct:marshalonlyinlineheaders").marshal().mimeMultipart("mixed", false, true, "(included|x-.*)", false);
                from("direct:unmarshalonly").unmarshal().mimeMultipart(false, false, false);
                from("direct:unmarshalonlyinlineheaders").streamCaching().unmarshal().mimeMultipart(false, true, false);
            }
        };
    }
}
