/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.commons.io.base64;

import org.testng.annotations.Test;

import java.io.IOException;
import java.util.Random;

/**
 * A test case for the <code>com.mucommander.commons.io.base64</code> package.
 *
 * @author Maxence Bernard, Nicolas Rinaudo
 */
public class Base64Test {

    /**
     * Tests base64 encoding and decoding on known sequences, ensuring that base 64 encoding and decoding produces
     * the expected results.
     *
     * @throws IOException should not happen
     */
    @Test
    public void testKnownSequences() throws IOException {
        // On an ASCII sequence
        testKnownSequence("The quick brown fox jumps over the lazy dog.", "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4=");

        // On a Unicode sequence. Note that the string has been encoded using UTF-8 bytes.
        testKnownSequence("どうもありがとうミスターロボット", "44Gp44GG44KC44GC44KK44GM44Go44GG44Of44K544K/44O844Ot44Oc44OD44OI");
    }

    /**
     * Tests base64 encoding and decoding on a known sequence, ensuring that base 64 encoding and decoding produces
     * the expected results.
     *
     * @param decodedSequence the base64-decoded string
     * @param encodedSequence the base64-encoding string
     * @throws IOException should not happen
     */
    private void testKnownSequence(String decodedSequence, String encodedSequence) throws IOException {
        assert encodedSequence.equals(Base64Encoder.encode(decodedSequence, "UTF-8", Base64Table.STANDARD_TABLE));
        assert decodedSequence.equals(Base64Decoder.decode(encodedSequence, "UTF-8", Base64Table.STANDARD_TABLE));
    }


    /**
     * Successively encodes and decodes randomly-generated strings of varying length and content, and verifies that
     * the resulting string remains the same as the original.
     *
     * @throws IOException should not happen
     */
    @Test
    public void testRandomStringIntegrity() throws IOException {
        Random random = new Random();

        StringBuffer sb;
        String s;
        int slen;
        // Repeats the test
        for(int i=0; i<100; i++) {
            // Generates a string with:
            // - a random length of up to 1000 characters
            // - random contents, where each byte's value is randomly chosen between 0 and 255
            slen = random.nextInt(1000);

            sb = new StringBuffer();
            for(int j=0; j<slen; j++)
                sb.append((char)random.nextInt(256));

            s = sb.toString();

            assert s.equals(Base64Decoder.decode(Base64Encoder.encode(s, "UTF-8", Base64Table.STANDARD_TABLE)));
        }
    }

    /**
     * Validates that <code>java.io.IOException</code> is properly thrown by {@link Base64InputStream}
     * when a character out of the base64 range is encountered. All such characters are successively tested.
     */
    @Test
    public void testInvalidCharacters() {
        char c;
        boolean exceptionCaught;

        for(c=0; c<256; c++) {
            // Skip allowed Base64 characters, including the special '=' character used for padding
            if((c>='0' && c<='9') || (c>='A' && c<='Z') || (c>='a' && c<='z') || c=='+' || c=='/' || c=='=')
                continue;

            exceptionCaught = false;

            try {
                Base64Decoder.decode(c+"===");      // Add padding at the end
            }
            catch(IOException e) {
                exceptionCaught = true;
            }

            assert exceptionCaught;
        }
    }

    /**
     * Validates that <code>java.io.IOException</code> is properly thrown by {@link Base64InputStream}
     * when the provided Base64 string's length is not a multiple of 4 (i.e. is not properly padded with '=').
     */
    @Test
    public void testInvalidLength() {
        String invalidLengthStrings[] = {
            "a", "ab", "abc",
            "=", "a=", "a==", "ab=",
            "0000a", "0000ab", "0000abc",
            "0000a=", "0000a==", "0000ab="
        };

        boolean exceptionCaught;

        for (String invalidLengthString : invalidLengthStrings) {
            exceptionCaught = false;

            try {
                Base64Decoder.decode(invalidLengthString);
            }
            catch (IOException e) {
                exceptionCaught = true;
            }

            assert exceptionCaught;
        }
    }


    /**
     * Tests {@link Base64Table} preset instances.
     *
     * @throws IOException should not happen
     */
    @Test
    public void testPresetTables() throws IOException {
        Base64Table[] tables = new Base64Table[] {
            Base64Table.STANDARD_TABLE,
            Base64Table.URL_SAFE_TABLE,
            Base64Table.FILENAME_SAFE_TABLE,
            Base64Table.REGEXP_SAFE_TABLE,
        };

        String sample = "The quick brown fox jumps over the lazy dog.";
        for(Base64Table table: tables) {
            // Ensure that the table passes Base64Table constructor's tests
            new Base64Table(table.getEncodingTable(), table.getPaddingChar());

            // Ensures that encoding followed by decoding yields the original string
            assert sample.equals(Base64Decoder.decode(Base64Encoder.encode(sample, "UTF-8", table), "UTF-8", table));
        }

    }

    /**
     * Tests {@link Base64Table#Base64Table(byte[], byte)} with invalid parameter values.
     */
    @Test
    public void testCustomBase64Table()  {
        testInvalidBase64Table(null, (byte)'a');
        testInvalidBase64Table(new byte[]{}, (byte)'a');

        byte[] validEncodingTable = Base64Table.STANDARD_TABLE.getEncodingTable();

        testInvalidBase64Table(validEncodingTable, (byte)'a');

        byte[] invalidEncodingTable = new byte[63];
        System.arraycopy(validEncodingTable, 0, invalidEncodingTable, 0, 63);

        testInvalidBase64Table(invalidEncodingTable, (byte)'a');

        invalidEncodingTable = new byte[64];
        System.arraycopy(validEncodingTable, 0, invalidEncodingTable, 0, 64);
        invalidEncodingTable[63] = 'b';

        testInvalidBase64Table(invalidEncodingTable, (byte)'a');

        // Test a valid custom Base64 table
        new Base64Table(new byte[]{
                'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
                'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
                'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
                'w','x','y','z','0','1','2','3','4','5','6','7','8','9','@','!'
        }, (byte)'%');
    }


    /**
     * Tries to create a <code>Base64Table</code> with the specified parameters and asserts that it throws
     * an {@link IllegalArgumentException}.
     *
     * @param table the base64 character table. The array must be 64 bytes long and must not contain any duplicate values.
         * @param paddingChar the ASCII character used for padding. This character must not already be used in the table.
         */
    private void testInvalidBase64Table(byte[] table, byte paddingChar) {
        boolean exceptionThrown = false;
        try {
            new Base64Table(table, paddingChar);
        }
        catch(IllegalArgumentException e) {
            exceptionThrown = true;
        }

        assert exceptionThrown;
    }
}
