package org.json.junit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.junit.Test;

/**
 * Test specific to the {@link org.json.JSONTokener} class.
 * @author John Aylward
 *
 */
public class JSONTokenerTest {

    /**
     * verify that back() fails as expected.
     * @throws IOException thrown if something unexpected happens.
     */
    @Test
    public void verifyBackFailureZeroIndex() throws IOException {
        try(Reader reader = new StringReader("some test string")) {
            final JSONTokener tokener = new JSONTokener(reader);
            try {
                // this should fail since the index is 0;
                tokener.back();
                fail("Expected an exception");
            } catch (JSONException e) {
                assertEquals("Stepping back two steps is not supported", e.getMessage());
            } catch (Exception e) {
                fail("Unknown Exception type " + e.getClass().getCanonicalName()+" with message "+e.getMessage());
            }
            
        }
    }
    /**
     * verify that back() fails as expected.
     * @throws IOException thrown if something unexpected happens.
     */
    @Test
    public void verifyBackFailureDoubleBack() throws IOException {
        try(Reader reader = new StringReader("some test string")) {
            final JSONTokener tokener = new JSONTokener(reader);
            tokener.next();
            tokener.back();
            try {
                // this should fail since the index is 0;
                tokener.back();
                fail("Expected an exception");
            } catch (JSONException e) {
                assertEquals("Stepping back two steps is not supported", e.getMessage());
            } catch (Exception e) {
                fail("Unknown Exception type " + e.getClass().getCanonicalName()+" with message "+e.getMessage());
            }
       }
    }
    
    @Test
    public void testValid() {
        checkValid("0",Number.class);
        checkValid(" 0  ",Number.class);
        checkValid("23",Number.class);
        checkValid("23.5",Number.class);
        checkValid(" 23.5  ",Number.class);
        checkValid("null",null);
        checkValid(" null  ",null);
        checkValid("true",Boolean.class);
        checkValid(" true\n",Boolean.class);
        checkValid("false",Boolean.class);
        checkValid("\nfalse  ",Boolean.class);
        checkValid("{}",JSONObject.class);
        checkValid(" {}  ",JSONObject.class);
        checkValid("{\"a\":1}",JSONObject.class);
        checkValid(" {\"a\":1}  ",JSONObject.class);
        checkValid("[]",JSONArray.class);
        checkValid(" []  ",JSONArray.class);
        checkValid("[1,2]",JSONArray.class);
        checkValid("\n\n[1,2]\n\n",JSONArray.class);
        checkValid("1 2", String.class);
    }
    
    @Test
    public void testErrors() {
        // Check that stream can detect that a value is found after
        // the first one
        checkError(" { \"a\":1 }  4 ");
        checkError("null \"a\"");
        checkError("{} true");
    }
    
    private Object checkValid(String testStr, Class<?> aClass)  {
        Object result = nextValue(testStr);

        // Check class of object returned
        if( null == aClass ) {
            if(JSONObject.NULL.equals(result)) {
                // OK
            } else {
                throw new JSONException("Unexpected class: "+result.getClass().getSimpleName());
            }
        } else {
            if( null == result ) {
                throw new JSONException("Unexpected null result");
            } else if(!aClass.isAssignableFrom(result.getClass()) ) {
                throw new JSONException("Unexpected class: "+result.getClass().getSimpleName());
            }
        }
        
        return result;
    }

    private void checkError(String testStr) {
        try {
            nextValue(testStr);
            
            fail("Error should be triggered: (\""+testStr+"\")");
        } catch (JSONException e) {
            // OK
        }
    }
    
    /**
     * Verifies that JSONTokener can read a stream that contains a value. After
     * the reading is done, check that the stream is left in the correct state
     * by reading the characters after. All valid cases should reach end of stream.
     * @param testStr
     * @return
     * @throws Exception
     */
    private Object nextValue(String testStr) throws JSONException {
        try(StringReader sr = new StringReader(testStr);){
            JSONTokener tokener = new JSONTokener(sr);
    
            Object result = tokener.nextValue();
    
            if( result == null ) {
                throw new JSONException("Unable to find value token in JSON stream: ("+tokener+"): "+testStr);
            }
            
            char c = tokener.nextClean();
            if( 0 != c ) {
                throw new JSONException("Unexpected character found at end of JSON stream: "+c+ " ("+tokener+"): "+testStr);
            }
    
            return result;
        }

    }
    
    /**
     * Tests the failure of the skipTo method with a buffered reader. Preferably
     * we'd like this not to fail but at this time we don't have a good recovery.
     * 
     * @throws IOException thrown if something unexpected happens.
     */
    @Test
    public void testSkipToFailureWithBufferedReader() throws IOException {
        final byte[] superLongBuffer = new byte[1000001];
        // fill our buffer
        for(int i=0;i<superLongBuffer.length;i++) {
            superLongBuffer[i] = 'A';
        }
        try(Reader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(superLongBuffer)))) {
            final JSONTokener tokener = new JSONTokener(reader);
            try {
                // this should fail since the internal markAhead buffer is only 1,000,000
                // but 'B' doesn't exist in our buffer that is 1,000,001 in size
                tokener.skipTo('B');
                fail("Expected exception");
            } catch (JSONException e) {
                assertEquals("Mark invalid", e.getMessage());
            } catch (Exception e) {
                fail("Unknown Exception type " + e.getClass().getCanonicalName()+" with message "+e.getMessage());
            }
        }
    }

    /**
     * Tests the success of the skipTo method with a String reader.
     * 
     * @throws IOException thrown if something unexpected happens.
     */
    @Test
    public void testSkipToSuccessWithStringReader() throws IOException {
        final StringBuilder superLongBuffer = new StringBuilder(1000001);
        // fill our buffer
        for(int i=0;i<superLongBuffer.length();i++) {
            superLongBuffer.append('A');
        }
        try(Reader reader = new StringReader(superLongBuffer.toString())) {
            final JSONTokener tokener = new JSONTokener(reader);
            try {
                // this should not fail since the internal markAhead is ignored for StringReaders
                tokener.skipTo('B');
            } catch (Exception e) {
                fail("Unknown Exception type " + e.getClass().getCanonicalName()+" with message "+e.getMessage());
            }
        }
    }

    /**
     * Verify that next and back are working properly and tracking the correct positions
     * with different new line combinations.
     */
    @Test
    public void testNextBackComboWithNewLines() {
        final String testString = "this is\nA test\r\nWith some different\rNew Lines";
        //                         ^       ^         ^                    ^
        // index positions         0       8        16                   36
        final JSONTokener tokener = new JSONTokener(testString);
        assertEquals(" at 0 [character 1 line 1]", tokener.toString());
        assertEquals('t',tokener.next());
        assertEquals(" at 1 [character 2 line 1]", tokener.toString());
        tokener.skipTo('\n');
        assertEquals("skipTo() improperly modifying indexes"," at 7 [character 8 line 1]", tokener.toString());
        assertEquals('\n',tokener.next());
        assertEquals(" at 8 [character 0 line 2]", tokener.toString());
        assertEquals('A',tokener.next());
        assertEquals(" at 9 [character 1 line 2]", tokener.toString());
        tokener.back();
        assertEquals(" at 8 [character 0 line 2]", tokener.toString());
        tokener.skipTo('\r');
        assertEquals("skipTo() improperly modifying indexes"," at 14 [character 6 line 2]", tokener.toString());
        // verify \r\n combo doesn't increment the line twice
        assertEquals('\r', tokener.next());
        assertEquals(" at 15 [character 0 line 3]", tokener.toString());
        assertEquals('\n', tokener.next());
        assertEquals(" at 16 [character 0 line 3]", tokener.toString());
        // verify stepping back after reading the \n of an \r\n combo doesn't  increment the line incorrectly
        tokener.back();
        assertEquals(" at 15 [character 6 line 2]", tokener.toString());
        assertEquals('\n', tokener.next());
        assertEquals(" at 16 [character 0 line 3]", tokener.toString());
        assertEquals('W', tokener.next());
        assertEquals(" at 17 [character 1 line 3]", tokener.toString());
        assertEquals('i', tokener.next());
        assertEquals(" at 18 [character 2 line 3]", tokener.toString());
        tokener.skipTo('\r');
        assertEquals("skipTo() improperly modifying indexes"," at 35 [character 19 line 3]", tokener.toString());
        assertEquals('\r', tokener.next());
        assertEquals(" at 36 [character 0 line 4]", tokener.toString());
        tokener.back();
        assertEquals(" at 35 [character 19 line 3]", tokener.toString());
        assertEquals('\r', tokener.next());
        assertEquals(" at 36 [character 0 line 4]", tokener.toString());
        assertEquals('N', tokener.next());
        assertEquals(" at 37 [character 1 line 4]", tokener.toString());
        
        // verify we get the same data just walking though, no calls to back
        final JSONTokener t2 = new JSONTokener(testString);
        for(int i=0; i<7; i++) {
            assertTrue(t2.toString().startsWith(" at " + i + " "));
            assertEquals(testString.charAt(i), t2.next());
        }
        assertEquals(" at 7 [character 8 line 1]", t2.toString());
        assertEquals(testString.charAt(7), t2.next());
        assertEquals(" at 8 [character 0 line 2]", t2.toString());
        for(int i=8; i<14; i++) {
            assertTrue(t2.toString().startsWith(" at " + i + " "));
            assertEquals(testString.charAt(i), t2.next());
        }
        assertEquals(" at 14 [character 6 line 2]", t2.toString());
        assertEquals('\r', t2.next());
        assertEquals(" at 15 [character 0 line 3]", t2.toString());
        assertEquals('\n', t2.next());
        assertEquals(" at 16 [character 0 line 3]", t2.toString());
        assertEquals('W', t2.next());
        assertEquals(" at 17 [character 1 line 3]", t2.toString());
        for(int i=17; i<37; i++) {
            assertTrue(t2.toString().startsWith(" at " + i + " "));
            assertEquals(testString.charAt(i), t2.next());
        }
        assertEquals(" at 37 [character 1 line 4]", t2.toString());
        for(int i=37; i<testString.length(); i++) {
            assertTrue(t2.toString().startsWith(" at " + i + " "));
            assertEquals(testString.charAt(i), t2.next());
        }
        assertEquals(" at "+ testString.length() +" [character 9 line 4]", t2.toString());
        // end of the input
        assertEquals(0, t2.next());
        assertFalse(t2.more());
   }
}