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

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;

import junit.framework.Test;
import junit.framework.TestSuite;

import org.apache.pdfbox.exceptions.COSVisitorException;
import org.apache.pdfbox.pdfwriter.COSWriter;

/**
 * This will test all of the filters in the PDFBox system.
 *
 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
 * @version $Revision$
 */
public class TestCOSString extends TestCOSBase
{
    private static final String ESC_CHAR_STRING =
            "( test#some) escaped< \\chars>!~1239857 ";
    private static final String ESC_CHAR_STRING_PDF_FORMAT =
            "\\( test#some\\) escaped< \\\\chars>!~1239857 ";

    /**
     * This will get the suite of test that this class holds.
     *
     * @return All of the tests that this class holds.
     */
    public static Test suite()
    {
        return new TestSuite(TestCOSString.class);
    }

    @Override
    public void setUp()
    {
        testCOSBase = new COSString("test cos string");
    }

    /**
     * infamous main method.
     *
     * @param args The command line arguments.
     */
    public static void main(String[] args)
    {
        String[] arg = {TestCOSString.class.getName()};
        junit.textui.TestRunner.main(arg);
    }

    /**
     * Tests the public static members within the class that are purely PDF format string objects
     * like open/closing strings, escape characters etc...
     */
    public void testStaticMembers()
    {
        stringByteArrayComparison("(", COSString.STRING_OPEN);
        stringByteArrayComparison(")", COSString.STRING_CLOSE);
        stringByteArrayComparison("<", COSString.HEX_STRING_OPEN);
        stringByteArrayComparison(">", COSString.HEX_STRING_CLOSE);
        stringByteArrayComparison("\\", COSString.ESCAPE);
        stringByteArrayComparison("\\r", COSString.CR_ESCAPE);
        stringByteArrayComparison("\\n", COSString.LF_ESCAPE);
        stringByteArrayComparison("\\t", COSString.HT_ESCAPE);
        stringByteArrayComparison("\\b", COSString.BS_ESCAPE);
        stringByteArrayComparison("\\f", COSString.FF_ESCAPE);
    }

    /**
     * Helper method for comparing a string to it's PDF byte array.
     *
     * @param expected the String expected
     * @param member the byte array being tested
     */
    private void stringByteArrayComparison(String expected, byte[] member)
    {
        byte[] expectedBytes = null;
        try
        {
            expectedBytes = expected.getBytes("ISO-8859-1");
        }
        catch (UnsupportedEncodingException e)
        {
            fail("ISO-8859-1 encoding threw an exception: " + e.getMessage());
        }
        testByteArrays(expectedBytes, member);
    }

    /**
     * Test setForceHexForm() and setForceLiteralForm() - tests these two methods do enforce the
     * different String output forms within PDF.
     */
    public void testSetForceHexLiteralForm()
    {
        String inputString = "Test with a text and a few numbers 1, 2 and 3";
        String pdfLiteral = "(" + inputString + ")";
        String pdfHex = "<" + createHex(inputString) + ">";
        COSString cosStr = new COSString(inputString);
        cosStr.setForceLiteralForm(true);
        writePDFTests(pdfLiteral, cosStr);
        cosStr.setForceHexForm(true);
        writePDFTests(pdfHex, cosStr);
        cosStr.setForceLiteralForm(true);
        writePDFTests(pdfLiteral, cosStr);

        COSString escStr = new COSString(ESC_CHAR_STRING);
        writePDFTests("(" + ESC_CHAR_STRING_PDF_FORMAT + ")", escStr);
        escStr.setForceHexForm(true);
        // Escape characters not escaped in hex version
        writePDFTests("<" + createHex(ESC_CHAR_STRING) + ">", escStr);
    }

    /**
     * Helper method for testing writePDF().
     *
     * @param expected the String expected when writePDF() is invoked
     * @param testSubj the test subject
     */
    private void writePDFTests(String expected, COSString testSubj)
    {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        try
        {
            testSubj.writePDF(outStream);
        }
        catch (IOException e)
        {
            fail("IOException: " + e.getMessage());
        }
        assertEquals(expected, outStream.toString());
    }

    /**
     * Test createFromHexString() - tests that the proper String is created from a hex string input.
     */
    public void testCreateFromHexString()
    {
        String expected = "Quick and simple test";
        String hexForm = createHex(expected);
        try
        {
            COSString test1 = COSString.createFromHexString(hexForm);
            writePDFTests("(" + expected + ")", test1);
            COSString test2 = COSString.createFromHexString(createHex(ESC_CHAR_STRING));
            writePDFTests("(" + ESC_CHAR_STRING_PDF_FORMAT + ")", test2);
            COSString test3 = COSString.createFromHexString(hexForm + "xx", true);
            writePDFTests("(" + expected + "?)", test3);
        }
        catch (IOException e)
        {
            fail("IOException thrown: " + e.getMessage());
        }
        try
        {
            COSString test4 = COSString.createFromHexString(hexForm + "xx", false);
            fail("Should have thrown an IOException here");
        }
        catch (IOException e)
        {
            // PASS
        }
    }

    private String createHex(String str)
    {
        StringBuilder sb = new StringBuilder();
        for (char c : str.toCharArray())
        {
            sb.append(Integer.toString(c, 16));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * Tests getHex() - ensure the hex String returned is properly formatted.
     */
    public void testGetHex()
    {
        String expected = "Test subject for testing getHex";
        COSString test1 = new COSString(expected);
        String hexForm = createHex(expected);
        assertEquals(hexForm, test1.getHexString());
        test1.setForceLiteralForm(true);
        assertEquals(hexForm, test1.getHexString());
        COSString escCS = new COSString(ESC_CHAR_STRING);
        // Not sure whether the escaped characters should be escaped or not, presumably since
        // writePDF() gives you the proper formatted text, getHex() should ONLY convert to hex.
        assertEquals(createHex(ESC_CHAR_STRING), escCS.getHexString());
    }

    /**
     * Test getString() - ensure string are returned in the correct format.
     */
    public void testGetString()
    {
        COSString nullStr = new COSString();
        assertEquals("", nullStr.getString());
        try
        {
            String testStr = "Test subject for getString()";
            COSString test1 = new COSString(testStr);
            assertEquals(testStr, test1.getString());

            String appendedStr = "appended text";
            test1.append(appendedStr.getBytes("ISO-8859-1"));
            assertEquals(testStr + appendedStr, test1.getString());

            test1.append(ESC_CHAR_STRING.getBytes("ISO-8859-1"));
            assertEquals(testStr + appendedStr + ESC_CHAR_STRING, test1.getString());

            COSString hexStr = COSString.createFromHexString(createHex(testStr));
            assertEquals(testStr, hexStr.getString());

            COSString escapedString = new COSString(ESC_CHAR_STRING);
            assertEquals(ESC_CHAR_STRING, escapedString.getString());

            testStr = "Line1\nLine2\nLine3\n";
            COSString lineFeedString = new COSString(testStr);
            assertEquals(testStr, lineFeedString.getString());

            //Same as previous but this time it is constructed incrementally (like in a dictionary)
            lineFeedString = new COSString();
            for (int i = 0; i < testStr.length(); i++)
            {
                lineFeedString.append(testStr.charAt(i));
            }
            assertEquals(testStr, lineFeedString.getString());

            testStr = "Text\u2026"; //PDFBOX-1437
            COSString pdfbox1437 = new COSString();
            pdfbox1437.append(new byte[] {
                    0x54, 0x65, 0x78, 0x74, (byte)(0x83 & 0xFF)
            });
            assertEquals(testStr, pdfbox1437.getString());
        }
        catch (IOException e)
        {
            fail("IOException thrown: " + e.getMessage());
        }
    }

    /**
     * Test append(int) and append(byte[]) - test both code paths.
     */
    public void testAppend()
    {
        try
        {
            // Mostly tested in testGetString()
            COSString testSubj = new COSString();
            StringBuilder sb = new StringBuilder();
            assertEquals(sb.toString(), testSubj.getString());
            // Arbitrary int but makes it easy to test
            testSubj.append('a');
            sb.append("a");
            assertEquals(sb.toString(), testSubj.getString());
            testSubj.append(ESC_CHAR_STRING.getBytes());
            sb.append(ESC_CHAR_STRING);
            assertEquals(sb.toString(), testSubj.getString());
            try
            {
                testSubj.append(null);
                assertEquals(sb.toString(), testSubj.getString());
                fail("NullPointerException not thrown.");
            }
            catch (NullPointerException e)
            {
                // PASS
            }
        }
        catch (IOException e)
        {
            fail("IOException thrown: " + e.getMessage());
        }
    }

    /**
     * Test reset() - tests that the internal buffer is reset. Not a great deal to test here...
     */
    public void testReset()
    {
        String str = "This string is going to be reset";
        COSString testSubj = new COSString(str);
        assertEquals(str, testSubj.getString());
        testSubj.reset();
        assertEquals("", testSubj.getString());
    }

    /**
     * Test getBytes() - again not much to test, just ensure the proper byte array is returned.
     */
    public void testGetBytes() throws UnsupportedEncodingException
    {
        COSString str = new COSString(ESC_CHAR_STRING);
        testByteArrays(ESC_CHAR_STRING.getBytes("ISO-8859-1"), str.getBytes());
    }

    /**
     * Tests writePDF() - tests that the string is in PDF format.
     */
    public void testWritePDF()
    {
        // This has been tested quite thorougly above but do a couple tests anyway
        COSString testSubj = new COSString(ESC_CHAR_STRING);
        writePDFTests("(" + ESC_CHAR_STRING_PDF_FORMAT + ")", testSubj);
        String textString = "This is just an arbitrary piece of text for testing";
        COSString testSubj2 = new COSString(textString);
        writePDFTests("(" + textString + ")", testSubj2);
    }

    /**
     * This will test all of the filters in the system.
     *
     * @throws IOException If there is an exception while encoding.
     */
    public void testUnicode() throws IOException
    {
        String theString = "\u4e16";
        COSString string = new COSString(theString);
        assertTrue(string.getString().equals(theString));
    }

    @Override
    public void testAccept()
    {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        ICOSVisitor visitor = new COSWriter(outStream);
        COSString testSubj = new COSString(ESC_CHAR_STRING);
        try
        {
            testSubj.accept(visitor);
            assertEquals("(" + ESC_CHAR_STRING_PDF_FORMAT + ")", outStream.toString());
            outStream.reset();
            testSubj.setForceHexForm(true);
            testSubj.accept(visitor);
            assertEquals("<" + createHex(ESC_CHAR_STRING) + ">", outStream.toString());
        }
        catch (COSVisitorException e)
        {
            fail(e.getMessage());
        }
    }

    /**
     * Tests equals(Object) - ensure that the Object.equals() contract is obeyed.
     */
    public void testEquals()
    {
        // Check all these several times for consistency
        for (int i = 0; i < 10; i++)
        {
            // Reflexive
            COSString x1 = new COSString("Test");
            assertTrue(x1.equals(x1));

            // Symmetry i.e. if x == y then y == x
            COSString y1 = new COSString("Test");
            assertTrue(x1.equals(y1));
            assertTrue(y1.equals(x1));
            COSString x2 = new COSString("Test");
            x2.setForceHexForm(true);
            // also if x != y then y != x
            assertFalse(x1.equals(x2));
            assertFalse(x2.equals(x1));

            // Transitive if x == y && y == z then x == z
            COSString z1 = new COSString("Test");
            assertTrue(x1.equals(y1));
            assertTrue(y1.equals(z1));
            assertTrue(x1.equals(z1));
            // Test the negative as well if x1 == y1 && y1 != x2 then x1 != x2
            assertTrue(x1.equals(y1));
            assertFalse(y1.equals(x2));
            assertFalse(x1.equals(x2));

            // Non-nullity
            assertFalse(x1.equals(null));
            assertFalse(y1.equals(null));
            assertFalse(z1.equals(null));
            assertFalse(x2.equals(null));

            // Also check other state
            COSString y2 = new COSString("Test");
            y2.setForceLiteralForm(true);
            assertFalse(y2.equals(x2));
            assertTrue(y2.equals(x1));
        }
    }

    /**
     * Test hashCode() - tests that the Object.hashCode() contract is obeyed.
     */
    public void testHashCode()
    {
        COSString str1 = new COSString("Test1");
        COSString str2 = new COSString("Test2");
        assertFalse(str1.hashCode() == str2.hashCode());
        COSString str3 = new COSString("Test1");
        assertTrue(str1.hashCode() == str3.hashCode());
        str3.setForceHexForm(true);
        assertFalse(str1.hashCode() == str3.hashCode());
        str3.setForceLiteralForm(true);
        assertTrue(str1.hashCode() == str3.hashCode());
    }
    
   /**
     * Test testCompareFromHexString() - tests that Strings created from hex
     * compare correctly (PDFBOX-2401)
     */
    public void testCompareFromHexString() throws IOException
    {
        COSString test1 = COSString.createFromHexString("000000FF000000");
        COSString test2 = COSString.createFromHexString("000000FF00FFFF");
        assertEquals(test1, test1);
        assertEquals(test2, test2);
        assertFalse(test1.getHexString().equals(test2.getHexString()));
        assertFalse(Arrays.equals(test1.getBytes(), test2.getBytes()));
        assertFalse(test1.equals(test2));
        assertFalse(test2.equals(test1));
        assertFalse(test1.getString().equals(test2.getString()));
    }
}
