/****************************************************************
 * 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.james.mime4j.parser;

import org.apache.james.mime4j.stream.BodyDescriptor;
import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig;
import org.apache.james.mime4j.util.ByteSequence;
import org.apache.james.mime4j.util.ContentUtil;
import org.junit.Assert;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.LinkedList;

public class MimeStreamParserTest {

    @Test
    public void testBoundaryInEpilogue() throws Exception {
        StringBuilder sb = new StringBuilder();
        sb.append("From: foo@bar.com\r\n");
        sb.append("To: someone@else.com\r\n");
        sb.append("Content-type: multipart/something; boundary=myboundary\r\n");
        sb.append("\r\n");
        sb.append("This is the preamble.\r\n");
        sb.append("--myboundary\r\n");
        sb.append("Content-type: text/plain\r\n");
        sb.append("\r\n");
        sb.append("This is the first body.\r\n");
        sb.append("It's completely meaningless.\r\n");
        sb.append("After this line the body ends.\r\n");
        sb.append("\r\n");
        sb.append("--myboundary--\r\n");

        StringBuilder epilogue = new StringBuilder();
        epilogue.append("Content-type: text/plain\r\n");
        epilogue.append("\r\n");
        epilogue.append("This is actually the epilogue but it looks like a second body.\r\n");
        epilogue.append("Yada yada yada.\r\n");
        epilogue.append("\r\n");
        epilogue.append("--myboundary--\r\n");
        epilogue.append("This is still the epilogue.\r\n");

        sb.append(epilogue.toString());

        ByteArrayInputStream bais = new ByteArrayInputStream(sb.toString().getBytes("US-ASCII"));

        final StringBuilder actual = new StringBuilder();

        MimeStreamParser parser = new MimeStreamParser();
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void epilogue(InputStream is) throws IOException {
                int b;
                while ((b = is.read()) != -1) {
                    actual.append((char) b);
                }
            }
        });
        parser.parse(bais);

        Assert.assertEquals(epilogue.toString(), actual.toString());
    }

    @Test
    public void testParseOneLineFields() throws Exception {
        StringBuilder sb = new StringBuilder();
        final LinkedList<String> expected = new LinkedList<String>();
        expected.add("From: foo@abr.com");
        sb.append(expected.getLast()).append("\r\n");
        expected.add("Subject: A subject");
        sb.append(expected.getLast()).append("\r\n");

        MimeStreamParser parser = new MimeStreamParser();
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void field(Field field) {
                Assert.assertEquals(expected.removeFirst(), decode(field.getRaw()));
            }
        });

        parser.parse(new ByteArrayInputStream(sb.toString().getBytes()));

        Assert.assertEquals(0, expected.size());
    }

    @Test
    public void testCRWithoutLFInHeader() throws Exception {
        /*
         * Test added because \r:s not followed by \n:s in the header would
         * cause an infinite loop.
         */
        StringBuilder sb = new StringBuilder();
        final LinkedList<String> expected = new LinkedList<String>();
        expected.add("The-field: This field\r\rcontains CR:s\r\r"
                + "not\r\n\tfollowed by LF");
        sb.append(expected.getLast()).append("\r\n");

        MimeStreamParser parser = new MimeStreamParser();
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void field(Field field) {
                Assert.assertEquals(expected.removeFirst(), decode(field.getRaw()));
            }
        });

        parser.parse(new ByteArrayInputStream(sb.toString().getBytes()));

        Assert.assertEquals(0, expected.size());
    }

    @Test
    public void testParseMultiLineFields() throws Exception {
        StringBuilder sb = new StringBuilder();
        final LinkedList<String> expected = new LinkedList<String>();
        expected.add("Received: by netmbx.netmbx.de (/\\==/\\ Smail3.1.28.1)\r\n"
                + "\tfrom mail.cs.tu-berlin.de with smtp\r\n"
                + "\tid &lt;m0uWPrO-0004wpC&gt;;"
                + " Wed, 19 Jun 96 18:12 MES");
        sb.append(expected.getLast()).append("\r\n");
        expected.add("Subject: A folded subject\r\n Line 2\r\n\tLine 3");
        sb.append(expected.getLast()).append("\r\n");

        MimeStreamParser parser = new MimeStreamParser();
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void field(Field field) {
                Assert.assertEquals(expected.removeFirst(), decode(field.getRaw()));
            }
        });

        parser.parse(new ByteArrayInputStream(sb.toString().getBytes()));

        Assert.assertEquals(0, expected.size());
    }

    @Test
    public void testStop() throws Exception {
        final MimeStreamParser parser = new MimeStreamParser();
        TestHandler handler = new TestHandler() {
            @Override
            public void endHeader() {
                super.endHeader();
                parser.stop();
            }
        };
        parser.setContentHandler(handler);

        String msg = "Subject: Yada yada\r\n"
                + "From: foo@bar.com\r\n"
                + "\r\n"
                + "Line 1\r\n"
                + "Line 2\r\n";
        String expected = "<message>\r\n"
                + "<header>\r\n"
                + "<field>\r\n"
                + "Subject: Yada yada"
                + "</field>\r\n"
                + "<field>\r\n"
                + "From: foo@bar.com"
                + "</field>\r\n"
                + "</header>\r\n"
                + "<body>\r\n"
                + "</body>\r\n"
                + "</message>\r\n";

        parser.parse(new ByteArrayInputStream(msg.getBytes()));
        String result = handler.sb.toString();

        Assert.assertEquals(expected, result);
    }

    /*
     * Tests that invalid fields are ignored.
     */
    @Test
    public void testInvalidFields() throws Exception {
        StringBuilder sb = new StringBuilder();
        final LinkedList<String> expected = new LinkedList<String>();
        sb.append("From - foo@abr.com\r\n");
        expected.add("From: some@one.com");
        sb.append(expected.getLast()).append("\r\n");
        expected.add("Subject: A subject");
        sb.append(expected.getLast()).append("\r\n");
        sb.append("A line which should be ignored\r\n");

        MimeStreamParser parser = new MimeStreamParser();
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void field(Field field) {
                Assert.assertEquals(expected.removeFirst(), decode(field.getRaw()));
            }
        });

        parser.parse(new ByteArrayInputStream(sb.toString().getBytes()));

        Assert.assertEquals(0, expected.size());
    }

    /*
     * Tests that empty streams still generate the expected series of events.
     */
    @Test
    public void testEmptyStream() throws Exception {
        final LinkedList<String> expected = new LinkedList<String>();
        expected.add("startMessage");
        expected.add("startHeader");
        expected.add("endHeader");
        expected.add("body");
        expected.add("endMessage");

        MimeStreamParser parser = new MimeStreamParser();
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void body(BodyDescriptor bd, InputStream is) {
                Assert.assertEquals(expected.removeFirst(), "body");
            }

            @Override
            public void endMultipart() {
                Assert.fail("endMultipart shouldn't be called for empty stream");
            }

            @Override
            public void endBodyPart() {
                Assert.fail("endBodyPart shouldn't be called for empty stream");
            }

            @Override
            public void endHeader() {
                Assert.assertEquals(expected.removeFirst(), "endHeader");
            }

            @Override
            public void endMessage() {
                Assert.assertEquals(expected.removeFirst(), "endMessage");
            }

            @Override
            public void field(Field field) {
                Assert.fail("field shouldn't be called for empty stream");
            }

            @Override
            public void startMultipart(BodyDescriptor bd) {
                Assert.fail("startMultipart shouldn't be called for empty stream");
            }

            @Override
            public void startBodyPart() {
                Assert.fail("startBodyPart shouldn't be called for empty stream");
            }

            @Override
            public void startHeader() {
                Assert.assertEquals(expected.removeFirst(), "startHeader");
            }

            @Override
            public void startMessage() {
                Assert.assertEquals(expected.removeFirst(), "startMessage");
            }
        });

        parser.parse(new ByteArrayInputStream(new byte[0]));

        Assert.assertEquals(0, expected.size());
    }

    /*
     * Tests parsing of empty headers.
     */
    @Test
    public void testEmpyHeader() throws Exception {
        StringBuilder sb = new StringBuilder();
        sb.append("\r\n");
        sb.append("The body is right here\r\n");

        final StringBuilder body = new StringBuilder();

        MimeStreamParser parser = new MimeStreamParser();
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void field(Field field) {
                Assert.fail("No fields should be reported");
            }

            @Override
            public void body(BodyDescriptor bd, InputStream is) throws IOException {
                int b;
                while ((b = is.read()) != -1) {
                    body.append((char) b);
                }
            }
        });

        parser.parse(new ByteArrayInputStream(sb.toString().getBytes()));

        Assert.assertEquals("The body is right here\r\n", body.toString());
    }

    /*
     * Tests parsing of empty body.
     */
    @Test
    public void testEmptyBody() throws Exception {
        StringBuilder sb = new StringBuilder();
        final LinkedList<String> expected = new LinkedList<String>();
        expected.add("From: some@one.com");
        sb.append(expected.getLast()).append("\r\n");
        expected.add("Subject: A subject");
        sb.append(expected.getLast()).append("\r\n\r\n");

        MimeStreamParser parser = new MimeStreamParser();
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void field(Field field) {
                Assert.assertEquals(expected.removeFirst(), decode(field.getRaw()));
            }

            @Override
            public void body(BodyDescriptor bd, InputStream is) throws IOException {
                Assert.assertEquals(-1, is.read());
            }
        });

        parser.parse(new ByteArrayInputStream(sb.toString().getBytes()));

        Assert.assertEquals(0, expected.size());
    }

    /*
     * Tests that invalid fields are ignored.
     */
    @Test
    public void testPrematureEOFAfterFields() throws Exception {
        StringBuilder sb = new StringBuilder();
        final LinkedList<String> expected = new LinkedList<String>();
        expected.add("From: some@one.com");
        sb.append(expected.getLast()).append("\r\n");
        expected.add("Subject: A subject");
        sb.append(expected.getLast());

        MimeStreamParser parser = new MimeStreamParser();
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void field(Field field) {
                Assert.assertEquals(expected.removeFirst(), decode(field.getRaw()));
            }
        });

        parser.parse(new ByteArrayInputStream(sb.toString().getBytes()));

        Assert.assertEquals(0, expected.size());

        sb = new StringBuilder();
        expected.clear();
        expected.add("From: some@one.com");
        sb.append(expected.getLast()).append("\r\n");
        expected.add("Subject: A subject");
        sb.append(expected.getLast()).append("\r\n");

        parser = new MimeStreamParser();
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void field(Field field) {
                Assert.assertEquals(expected.removeFirst(), decode(field.getRaw()));
            }
        });

        parser.parse(new ByteArrayInputStream(sb.toString().getBytes()));

        Assert.assertEquals(0, expected.size());
    }

    @Test
    public void testAutomaticContentDecoding() throws Exception {
        MimeStreamParser parser = new MimeStreamParser();
        parser.setContentDecoding(true);
        TestHandler handler = new TestHandler();
        parser.setContentHandler(handler);

        String msg = "Subject: Yada yada\r\n"
                + "From: foo@bar.com\r\n"
                + "Content-Type: application/octet-stream\r\n"
                + "Content-Transfer-Encoding: base64\r\n"
                + "\r\n"
                + "V2hvIGF0ZSBteSBjYWtlPwo=";
        String expected = "<message>\r\n"
                + "<header>\r\n"
                + "<field>\r\n"
                + "Subject: Yada yada"
                + "</field>\r\n"
                + "<field>\r\n"
                + "From: foo@bar.com"
                + "</field>\r\n"
                + "<field>\r\n"
                + "Content-Type: application/octet-stream"
                + "</field>\r\n"
                + "<field>\r\n"
                + "Content-Transfer-Encoding: base64"
                + "</field>\r\n"
                + "</header>\r\n"
                + "<body>\r\n"
                + "Who ate my cake?\n"
                + "</body>\r\n"
                + "</message>\r\n";

        parser.parse(new ByteArrayInputStream(msg.getBytes()));
        String result = handler.sb.toString();

        Assert.assertEquals(expected, result);
    }

    @Test
    public void testRfc6531() throws Exception {
        MimeStreamParser parser = new MimeStreamParser(MimeConfig.STRICT);
        parser.setContentDecoding(true);
        TestHandler handler = new TestHandler();
        parser.setContentHandler(handler);

        String msg = "Subject: Naïve Subject\r\n"
                + "From: foo@ø.example\r\n"
                + "To: ø@example.com\r\n"
                + "Content-Type: text/plain; charset=utf-8\r\n"
                + "\r\n"
                + "This sentence ends with the letter x.\r\n";
        String expected = "<message>\r\n"
                + "<header>\r\n"
                + "<field>\r\n"
                + "Subject: Naïve Subject"
                + "</field>\r\n"
                + "<field>\r\n"
                + "From: foo@ø.example"
                + "</field>\r\n"
                + "<field>\r\n"
                + "To: ø@example.com"
                + "</field>\r\n"
                + "<field>\r\n"
                + "Content-Type: text/plain; charset=utf-8"
                + "</field>\r\n"
                + "</header>\r\n"
                + "<body>\r\n"
                + "This sentence ends with the letter x.\r\n"
                + "</body>\r\n"
                + "</message>\r\n";

        // Dot the ı's and check that the ø is present in the message
        // as its UTF8 encoding, 0xC3 0xB8. If the test uses anything
        // else, then passing the test doesn't imply correctness.
        byte[] msgAsUtf8 = msg.getBytes(Charset.forName("utf8"));
        int i = 0;
        while(i+1 < msgAsUtf8.length &&
              (msgAsUtf8[i] != 0xC3 || msgAsUtf8[i+1] != 0xB8))
              i++;
        Assert.assertTrue(i < msgAsUtf8.length);

        parser.parse(new ByteArrayInputStream(msgAsUtf8));
        String result = handler.sb.toString();

        Assert.assertEquals(expected, result);
    }

    protected String decode(ByteSequence byteSequence) {
        return ContentUtil.decode(byteSequence);
    }

}
