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

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

import junit.framework.AssertionFailedError;
import junit.framework.TestCase;

public final class Base64Test extends TestCase {

    public void testEncodeDecode() throws Exception {
        assertEncodeDecode("");
        assertEncodeDecode("Eg==", 0x12);
        assertEncodeDecode("EjQ=", 0x12, 0x34);
        assertEncodeDecode("EjRW", 0x12, 0x34, 0x56);
        assertEncodeDecode("EjRWeA==", 0x12, 0x34, 0x56, 0x78);
        assertEncodeDecode("EjRWeJo=", 0x12, 0x34, 0x56, 0x78, 0x9A);
        assertEncodeDecode("EjRWeJq8", 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc);
    }

    public void testEncode_doesNotWrap() throws Exception {
        int[] data = new int[61];
        Arrays.fill(data, 0xff);
        String expected = "///////////////////////////////////////////////////////////////////////"
                + "//////////w=="; // 84 chars
        assertEncodeDecode(expected, data);
    }

    private static void assertEncodeDecode(String expectedEncoded, int... toEncode)
            throws Exception {
        // We should never expect (or receive) non-ASCII text from Base64.encoder.
        asciiToBytes(expectedEncoded);

        // Convert the convenient ints to the bytes we need.
        byte[] inputBytes = new byte[toEncode.length];
        for (int i = 0; i < toEncode.length; i++) {
            inputBytes[i] = (byte) toEncode[i];
        }
        String encoded = Base64.encode(inputBytes);
        assertEquals(expectedEncoded, encoded);

        // Check we can round-trip the encoded bytes to
        // arrive at what we started with.
        int[] actualDecodedBytes = decodeToInts(encoded);
        assertArrayEquals(toEncode, actualDecodedBytes);
    }

    public void testDecode_empty() throws Exception {
        byte[] decoded = Base64.decode(new byte[0]);
        assertEquals(0, decoded.length);
    }

    public void testDecode_truncated() throws Exception {
        // Correct data, for reference.
        assertEquals("hello, world", decodeToString("aGVsbG8sIHdvcmxk"));

        // The following are missing the final bytes
        assertEquals("hello, wo", decodeToString("aGVsbG8sIHdvcmx"));
        assertEquals("hello, wo", decodeToString("aGVsbG8sIHdvcm"));
        assertEquals("hello, wo", decodeToString("aGVsbG8sIHdvc"));
        assertEquals("hello, wo", decodeToString("aGVsbG8sIHdv"));
    }

    public void testDecode_extraChars() throws Exception {
        // Characters outside of alphabet before padding.
        assertEquals("hello, world", decodeToString(" aGVsbG8sIHdvcmxk"));
        assertEquals("hello, world", decodeToString("aGV sbG8sIHdvcmxk"));
        assertEquals("hello, world", decodeToString("aGVsbG8sIHdvcmxk "));
        assertEquals(null, decodeToString("*aGVsbG8sIHdvcmxk"));
        assertEquals(null, decodeToString("aGV*sbG8sIHdvcmxk"));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxk*"));
        assertEquals("hello, world", decodeToString("\r\naGVsbG8sIHdvcmxk"));
        assertEquals("hello, world", decodeToString("aGV\r\nsbG8sIHdvcmxk"));
        assertEquals("hello, world", decodeToString("aGVsbG8sIHdvcmxk\r\n"));
        assertEquals("hello, world", decodeToString("\naGVsbG8sIHdvcmxk"));
        assertEquals("hello, world", decodeToString("aGV\nsbG8sIHdvcmxk"));
        assertEquals("hello, world", decodeToString("aGVsbG8sIHdvcmxk\n"));

        // padding 0
        assertEquals("hello, world", decodeToString("aGVsbG8sIHdvcmxk"));
        // Extra padding
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxk="));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxk=="));
        // Characters outside alphabet intermixed with (too much) padding.
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxk ="));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxk = = "));

        // padding 1
        assertEquals("hello, world?!", decodeToString("aGVsbG8sIHdvcmxkPyE="));
        // Missing padding
        assertEquals("hello, world", decodeToString("aGVsbG8sIHdvcmxkPyE"));
        // Characters outside alphabet before padding.
        assertEquals("hello, world?!", decodeToString("aGVsbG8sIHdvcmxkPyE ="));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkPyE*="));
        // Trailing characters, otherwise valid.
        assertEquals("hello, world?!", decodeToString("aGVsbG8sIHdvcmxkPyE= "));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkPyE=*"));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkPyE=X"));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkPyE=XY"));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkPyE=XYZ"));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkPyE=XYZA"));
        assertEquals("hello, world?!", decodeToString("aGVsbG8sIHdvcmxkPyE=\n"));
        assertEquals("hello, world?!", decodeToString("aGVsbG8sIHdvcmxkPyE=\r\n"));
        assertEquals("hello, world?!", decodeToString("aGVsbG8sIHdvcmxkPyE= "));
        assertEquals("hello, world?!", decodeToString("aGVsbG8sIHdvcmxkPyE=="));
        // Whitespace characters outside alphabet intermixed with (too much) padding.
        assertEquals("hello, world?!", decodeToString("aGVsbG8sIHdvcmxkPyE =="));
        assertEquals("hello, world?!", decodeToString("aGVsbG8sIHdvcmxkPyE = = "));

        // padding 2
        assertEquals("hello, world.", decodeToString("aGVsbG8sIHdvcmxkLg=="));
        // Missing padding
        assertEquals("hello, world", decodeToString("aGVsbG8sIHdvcmxkLg"));
        // Partially missing padding
        assertEquals("hello, world", decodeToString("aGVsbG8sIHdvcmxkLg="));
        // Characters outside alphabet before padding.
        assertEquals("hello, world.", decodeToString("aGVsbG8sIHdvcmxkLg =="));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkLg*=="));
        // Trailing characters, otherwise valid.
        assertEquals("hello, world.", decodeToString("aGVsbG8sIHdvcmxkLg== "));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkLg==*"));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkLg==X"));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkLg==XY"));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkLg==XYZ"));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkLg==XYZA"));
        assertEquals("hello, world.", decodeToString("aGVsbG8sIHdvcmxkLg==\n"));
        assertEquals("hello, world.", decodeToString("aGVsbG8sIHdvcmxkLg==\r\n"));
        assertEquals("hello, world.", decodeToString("aGVsbG8sIHdvcmxkLg== "));
        assertEquals("hello, world.", decodeToString("aGVsbG8sIHdvcmxkLg==="));
        // Characters outside alphabet inside padding.
        assertEquals("hello, world.", decodeToString("aGVsbG8sIHdvcmxkLg= ="));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkLg=*="));
        assertEquals("hello, world.", decodeToString("aGVsbG8sIHdvcmxkLg=\r\n="));
        // Characters inside alphabet inside padding.
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmxkLg=X="));

        // Table 1 chars
        assertEquals(null, decodeToString("_aGVsbG8sIHdvcmx"));
        assertEquals(null, decodeToString("aGV_sbG8sIHdvcmx"));
        assertEquals(null, decodeToString("aGVsbG8sIHdvcmx_"));

        // Table 2 chars.
        assertArrayEquals(
                new int[] {0xfd, 0xa1, 0x95, 0xb1, 0xb1, 0xbc, 0xb0, 0x81, 0xdd, 0xbd, 0xc9,
                        0xb1 },
                decodeToInts("/aGVsbG8sIHdvcmx"));
        assertArrayEquals(
                new int[] { 0x68, 0x65, 0x7f, 0xb1, 0xb1, 0xbc, 0xb0, 0x81, 0xdd, 0xbd, 0xc9,
                        0xb1 },
                decodeToInts("aGV/sbG8sIHdvcmx"));
        assertArrayEquals(
                new int[] { 104, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 0x7f },
                decodeToInts("aGVsbG8sIHdvcmx/"));
    }

    private static final int[] BYTE_VALUES = {
            0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77
    };

    public void testDecode_nonAsciiBytes() throws Exception {
        assertSubArrayEquals(BYTE_VALUES, 0, decodeToInts(""));
        assertSubArrayEquals(BYTE_VALUES, 1, decodeToInts("/w=="));
        assertSubArrayEquals(BYTE_VALUES, 2, decodeToInts("/+4="));
        assertSubArrayEquals(BYTE_VALUES, 3, decodeToInts("/+7d"));
        assertSubArrayEquals(BYTE_VALUES, 4, decodeToInts("/+7dzA=="));
        assertSubArrayEquals(BYTE_VALUES, 5, decodeToInts("/+7dzLs="));
        assertSubArrayEquals(BYTE_VALUES, 6, decodeToInts("/+7dzLuq"));
        assertSubArrayEquals(BYTE_VALUES, 7, decodeToInts("/+7dzLuqmQ=="));
        assertSubArrayEquals(BYTE_VALUES, 8, decodeToInts("/+7dzLuqmYg="));
    }

    public void testDecode_urlAlphabet() throws Exception {
        assertNull(decodeToInts("_w=="));
        assertNull(decodeToInts("-w=="));
    }

    /**
     * Convenience function for decoding from a Base64 ASCII String to an ASCII String. A String is
     * used for the output to make the tests compact. Can return null if the decoder returns null.
     * If any of the strings involved are non-ASCII an exception is thrown.
     * Use {@link #decodeToInts(String)} for decode tests that produce bytes
     * outside of the ASCII range.
     */
    private static String decodeToString(String in) throws Exception {
        byte[] bytes = asciiToBytes(in);
        byte[] out = Base64.decode(bytes);
        if (out == null) {
            return null;
        }
        return bytesToAscii(out);
    }

    private static String bytesToAscii(byte[] bytes) {
        try {
            CharsetDecoder decoder = StandardCharsets.US_ASCII.newDecoder();
            decoder.onMalformedInput(CodingErrorAction.REPORT);
            decoder.onUnmappableCharacter(CodingErrorAction.REPORT);
            ByteBuffer bytesBuffer = ByteBuffer.wrap(bytes);
            CharBuffer charsBuffer = decoder.decode(bytesBuffer);
            char[] chars = new char[charsBuffer.remaining()];
            charsBuffer.get(chars, 0, chars.length);
            return new String(chars);
        } catch (CharacterCodingException e) {
            // Use bytes in your test, not Strings.
            throw new AssertionFailedError("Cannot convert test bytes to String safely: " +
                    Arrays.toString(bytesToInts(bytes)) + " contains non-ASCII codes");
        }
    }

    private static byte[] asciiToBytes(String string) {
        try {
            char[] chars = string.toCharArray();

            CharsetEncoder encoder = StandardCharsets.US_ASCII.newEncoder();
            encoder.onMalformedInput(CodingErrorAction.REPORT);
            encoder.onUnmappableCharacter(CodingErrorAction.REPORT);
            CharBuffer charsBuffer = CharBuffer.wrap(chars);
            ByteBuffer bytesBuffer = encoder.encode(charsBuffer);
            byte[] bytes = new byte[bytesBuffer.remaining()];
            bytesBuffer.get(bytes, 0, bytes.length);
            return bytes;
        } catch (CharacterCodingException e) {
            // Use bytes in your test, not Strings.
            throw new AssertionFailedError("Cannot convert test String to bytes safely: " + string +
                    " contains non-ASCII characters");
        }
    }

    /** Decodes an ASCII string, returning an int array. */
    private static int[] decodeToInts(String in) throws Exception {
        byte[] bytes = Base64.decode(asciiToBytes(in));
        return bytesToInts(bytes);
    }

    /**
     * Convert a byte[] to an int[]. int is used because it is more convenient to use ints in
     * tests.
     */
    private static int[] bytesToInts(byte[] bytes) {
        if (bytes == null) {
            return null;
        }
        int[] ints = new int[bytes.length];
        for (int i = 0; i < bytes.length; i++) {
            ints[i] = bytes[i] & 0xff;
        }
        return ints;
    }

    /** Assert that decoding 'in' throws ArrayIndexOutOfBoundsException. */
    private static void assertDecodeBad(String in) throws Exception {
        try {
            byte[] result = Base64.decode(asciiToBytes(in));
            fail("should have failed to decode. Actually received: " +
                    (result == null ? result : Arrays.toString(bytesToInts(result))));
        } catch (ArrayIndexOutOfBoundsException e) {
        }
    }

    private static void assertArrayEquals(int[] expected, int[] actual) {
        assertSubArrayEquals(expected, expected.length, actual);
    }

    /** Assert that actual equals the first len bytes of expected. */
    private static void assertSubArrayEquals(int[] expected, int len, int[] actual) {
        // Convert the arrays to Strings for easy comparison / reporting.
        String expectedString = intsToString(expected, len);
        String actualString = intsToString(actual, actual.length);
        assertEquals(expectedString, actualString);
    }

    private static String intsToString(int[] toConvert, int length) {
        String[] out = new String[length];
        for (int i = 0; i < length; i++) {
            out[i] = "0x" + Integer.toHexString(toConvert[i]);
        }
        return Arrays.toString(out);
    }
}

