/****************************************************************
 * 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.codec;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Random;

import org.apache.commons.io.output.NullOutputStream;
import org.apache.log4j.BasicConfigurator;

import junit.framework.TestCase;

public class Base64InputStreamTest extends TestCase {

    @Override
    public void setUp() {
        BasicConfigurator.resetConfiguration();
        BasicConfigurator.configure();
    }
    
    public void testDecode() throws IOException {
        ByteArrayInputStream bis = null;
        Base64InputStream decoder = null;
        byte[] bytes = null;
        
        /*
         * Simple initial test.
         */
        bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlIQ=="));
        decoder = new Base64InputStream(bis);
        assertEquals("This is the plain text message!", toString(read(decoder)));
        
        /*
         * Test encoded text padded once, twice and not at all.
         */
        bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyBhIHRleHQgd2hpY2ggaGFzIHRvIGJl"
                        + "IHBhZGRlZCBvbmNlLi4="));
        decoder = new Base64InputStream(bis);
        assertEquals("This is a text which has to be padded once..", toString(read(decoder)));
        bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyBhIHRleHQgd2hpY2ggaGFzIHRvIGJl"
                        + "IHBhZGRlZCB0d2ljZQ=="));
        decoder = new Base64InputStream(bis);
        assertEquals("This is a text which has to be padded twice", toString(read(decoder)));
        bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyBhIHRleHQgd2hpY2ggd2lsbCBub3Qg"
                        + "YmUgcGFkZGVk"));
        decoder = new Base64InputStream(bis);
        assertEquals("This is a text which will not be padded", toString(read(decoder)));
        
        /*
         * Test that non base64 characters are ignored.
         */
        bis = new ByteArrayInputStream(
                fromString(" &% VGhp\r\ncyBp\r\ncyB0aGUgcGxhaW4g "
                        + " \tdGV4dCBtZ?!XNzY*WdlIQ=="));
        decoder = new Base64InputStream(bis);
        assertEquals("This is the plain text message!", toString(read(decoder)));
        
        /*
         * Test that the bytes 0-255 shifted 0, 1 and 2 positions are
         * decoded properly.
         */
        String s1 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCU"
                  + "mJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0"
                  + "xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3Bxc"
                  + "nN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeY"
                  + "mZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6"
                  + "/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5O"
                  + "Xm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==";
        
        String s2 = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSY"
                  + "nKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE"
                  + "1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc"
                  + "3R1dnd4eXp7fH1+f4CBgoOEhYaHiImKi4yNjo+QkZKTlJWWl5iZ"
                  + "mpucnZ6foKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u7y9vr/"
                  + "AwcLDxMXGx8jJysvMzc7P0NHS09TV1tfY2drb3N3e3+Dh4uPk5e"
                  + "bn6Onq6+zt7u/w8fLz9PX29/j5+vv8/f7/AA==";
        
        String s3 = "AgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICEiIyQlJic"
                  + "oKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU"
                  + "5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzd"
                  + "HV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJma"
                  + "m5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8D"
                  + "BwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5u"
                  + "fo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8AAQ==";
        
        bis = new ByteArrayInputStream(fromString(s1));
        decoder = new Base64InputStream(bis);
        bytes = read(decoder);
        
        for (int i = 0; i < bytes.length; i++) {
            assertEquals("Position " + i, bytes[i], (byte) i);
        }
        
        bis = new ByteArrayInputStream(fromString(s2));
        decoder = new Base64InputStream(bis);
        bytes = read(decoder);
        
        for (int i = 0; i < bytes.length; i++) {
            assertEquals("Position " + i, bytes[i], (byte) (i + 1));
        }
        
        bis = new ByteArrayInputStream(fromString(s3));
        decoder = new Base64InputStream(bis);
        bytes = read(decoder);
        
        for (int i = 0; i < bytes.length; i++) {
            assertEquals("Position " + i, bytes[i], (byte) (i + 2));
        }
    }

    public void testDecodePrematureClose() throws IOException {
        ByteArrayInputStream bis = null;
        Base64InputStream decoder = null;
        
        bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlIQ=="));
        decoder = new Base64InputStream(bis);
        assertEquals('T', decoder.read());
        assertEquals('h', decoder.read());
        decoder.close();
        
        try {
            decoder.read();
            fail();
        } catch (IOException expected) {
        }
    }

    public void testRoundtripWithVariousBufferSizes() throws Exception {
        byte[] data = new byte[3719];
        new Random(0).nextBytes(data);

        ByteArrayOutputStream eOut = new ByteArrayOutputStream();
        CodecUtil.encodeBase64(new ByteArrayInputStream(data), eOut);
        byte[] encoded = eOut.toByteArray();

        for (int bufferSize = 1; bufferSize <= 1009; bufferSize++) {
            ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
            Base64InputStream decoder = new Base64InputStream(bis);
            ByteArrayOutputStream dOut = new ByteArrayOutputStream();

            final byte[] buffer = new byte[bufferSize];
            int inputLength;
            while (-1 != (inputLength = decoder.read(buffer))) {
                dOut.write(buffer, 0, inputLength);
            }

            byte[] decoded = dOut.toByteArray();

            assertEquals(data.length, decoded.length);
            for (int i = 0; i < data.length; i++) {
                assertEquals(data[i], decoded[i]);
            }
        }
    }

    /**
     * Tests {@link InputStream#read()}
     */
    public void testReadInt() throws Exception {
        ByteArrayInputStream bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlIQ=="));
        Base64InputStream decoder = new Base64InputStream(bis);
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        while (true) {
            int x = decoder.read();
            if (x == -1)
                break;
            out.write(x);
        }

        assertEquals("This is the plain text message!", toString(out
                .toByteArray()));
    }

    /**
     * Tests {@link InputStream#read(byte[], int, int)} with various offsets
     */
    public void testReadOffset() throws Exception {
        ByteArrayInputStream bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlIQ=="));
        Base64InputStream decoder = new Base64InputStream(bis);

        byte[] data = new byte[36];
        for (int i = 0;;) {
            int bytes = decoder.read(data, i, 5);
            if (bytes == -1)
                break;
            i += bytes;
        }

        assertEquals("This is the plain text message!\0\0\0\0\0",
                toString(data));
    }

    public void testStrictUnexpectedEof() throws Exception {
        ByteArrayInputStream bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlI"));
        Base64InputStream decoder = new Base64InputStream(bis, true);
        try {
            CodecUtil.copy(decoder, new NullOutputStream());
            fail();
        } catch (IOException expected) {
            assertTrue(expected.getMessage().toLowerCase().contains(
                    "end of file"));
        }
    }

    public void testLenientUnexpectedEof() throws Exception {
        ByteArrayInputStream bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlI"));
        Base64InputStream decoder = new Base64InputStream(bis, false);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        CodecUtil.copy(decoder, out);
        assertEquals("This is the plain text message", toString(out
                .toByteArray()));
    }

    public void testStrictUnexpectedPad() throws Exception {
        ByteArrayInputStream bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlI="));
        Base64InputStream decoder = new Base64InputStream(bis, true);
        try {
            CodecUtil.copy(decoder, new NullOutputStream());
            fail();
        } catch (IOException expected) {
            assertTrue(expected.getMessage().toLowerCase().contains("pad"));
        }
    }

    public void testLenientUnexpectedPad() throws Exception {
        ByteArrayInputStream bis = new ByteArrayInputStream(
                fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlI="));
        Base64InputStream decoder = new Base64InputStream(bis, false);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        CodecUtil.copy(decoder, out);
        assertEquals("This is the plain text message", toString(out
                .toByteArray()));
    }
        
    private byte[] read(InputStream is) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        int b;
        while ((b = is.read()) != -1) {
            bos.write(b);
        }
        return bos.toByteArray();
    }
    
    private byte[] fromString(String s) {
        try {
            return s.getBytes("US-ASCII");
        } catch (UnsupportedEncodingException e) {
            fail(e.getMessage());
            return null;
        }
    }
    
    private String toString(byte[] b) {
        try {
            return new String(b, "US-ASCII");
        } catch (UnsupportedEncodingException e) {
            fail(e.getMessage());
            return null;
        }
    }
}
