/*
 * Copyright (c) 2008-2014 MongoDB, Inc.
 *
 * Licensed 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 com.mongodb.util;

import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBObject;
import org.bson.BSON;
import org.bson.BasicBSONObject;
import org.bson.types.BSONTimestamp;
import org.bson.types.Code;
import org.bson.types.CodeWScope;
import org.bson.types.ObjectId;
import org.junit.Test;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.SimpleTimeZone;
import java.util.UUID;
import java.util.regex.Pattern;

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

@SuppressWarnings("deprecation")
public class JSONTest {

    @Test
    public void testSerializationMethods() {

        // basic test of each of JSON class' serialization methods
        String json = "{ \"x\" : \"basic test\"}";
        Object obj = JSON.parse(json);

        assertEquals(json, JSON.serialize(obj));
    }

    @Test
    public void testNumbers() {
        assertEquals("{ \"x\" : 5}", JSON.serialize(JSON.parse("{'x' : 5 }")));
        assertEquals("{ \"x\" : 5.0}", JSON.serialize(JSON.parse("{'x' : 5.0 }")));
        assertEquals("{ \"x\" : 0}", JSON.serialize(JSON.parse("{'x' : 0 }")));
        assertEquals("{ \"x\" : 0.0}", JSON.serialize(JSON.parse("{'x' : 0.0 }")));
        assertEquals("{ \"x\" : 500}", JSON.serialize(JSON.parse("{'x' : 500 }")));
        assertEquals("{ \"x\" : 500.0}", JSON.serialize(JSON.parse("{'x' : 500.0 }")));
        assertEquals("{ \"x\" : 0.5}", JSON.serialize(JSON.parse("{'x' : 0.500 }")));
        assertEquals("{ \"x\" : 5.0}", JSON.serialize(JSON.parse("{'x' : 5. }")));
        assertEquals("{ \"x\" : 50.0}", JSON.serialize(JSON.parse("{'x' : 5.0e+1 }")));
        assertEquals("{ \"x\" : 0.5}", JSON.serialize(JSON.parse("{'x' : 5.0E-1 }")));
    }

    @Test
    public void testLongValues() {
        Long bigVal = Integer.MAX_VALUE + 1L;
        String test = String.format("{ \"x\" : %d}", bigVal);
        assertEquals(test, JSON.serialize(JSON.parse(test)));

        Long smallVal = Integer.MIN_VALUE - 1L;
        String test2 = String.format("{ \"x\" : %d}", smallVal);
        assertEquals(test2, JSON.serialize(JSON.parse(test2)));

        try {
            JSON.parse("{\"ReallyBigNumber\": 10000000000000000000 }");
            fail("JSONParseException should have been thrown");
        } catch (JSONParseException e) {
            // fall through
        }
    }

    @Test
    public void testSimple() {
        assertEquals("{ \"csdf\" : true}", JSON.serialize(JSON.parse("{'csdf' : true}")));
        assertEquals("{ \"csdf\" : false}", JSON.serialize(JSON.parse("{'csdf' : false}")));
        assertEquals("{ \"csdf\" :  null }", JSON.serialize(JSON.parse("{'csdf' : null}")));
        assertEquals("{ \"id\" : \"1689c12eb234c54a84ebd100\"}",
                     JSON.serialize(JSON.parse("{\n\t\"id\":\"1689c12eb234c54a84ebd100\",\n}")));
    }

    @Test
    public void testString() {
        assertEquals("{ \"csdf\" : \"foo\"}", JSON.serialize(JSON.parse("{'csdf' : \"foo\"}")));
        assertEquals("{ \"csdf\" : \"foo\"}", JSON.serialize(JSON.parse("{'csdf' : \'foo\'}")));
        assertEquals("{ \"csdf\" : \"a\\\"b\"}", JSON.serialize(JSON.parse("{'csdf' : \"a\\\"b\"}")));
    }

    @Test
    public void testArray() {
        assertEquals("{ \"csdf\" : [ \"foo\"]}", JSON.serialize(JSON.parse("{'csdf' : [\"foo\"]}")));
        assertEquals("{ \"csdf\" : [ 3 , 5 , \"foo\" ,  null ]}", JSON.serialize(JSON.parse("{'csdf' : [3, 5, \'foo\', null]}")));
        assertEquals("{ \"csdf\" : [ 3.0 , 5.0 , \"foo\" ,  null ]}", JSON.serialize(JSON.parse("{'csdf' : [3.0, 5.0, \'foo\', null]}")));
        assertEquals("{ \"csdf\" : [ [ ] , [ [ ]] , false]}", JSON.serialize(JSON.parse("{'csdf' : [[],[[]],false]}")));
    }

    @Test
    public void testObject() {
        assertEquals("{ \"csdf\" : { }}", JSON.serialize(JSON.parse("{'csdf' : {}}")));
        assertEquals("{ \"csdf\" : { \"foo\" : \"bar\"}}", JSON.serialize(JSON.parse("{'csdf' : {\"foo\":\"bar\"}}")));
        assertEquals("{ \"csdf\" : { \"hi\" : { \"hi\" : [ { }]}}}", JSON.serialize(JSON.parse("{'csdf' : {\'hi\':{\'hi\':[{}]}}}")));
    }

    @Test
    public void testMulti() {
        assertEquals("{ \"\" : \"\" , \"34\" : -52.5}", JSON.serialize(JSON.parse("{\'\' : \"\", \"34\" : -52.5}")));
    }

    @Test
    public void testUnicode() {
        assertEquals("{ \"x\" : \"hi \"}", JSON.serialize(JSON.parse("{'x' : \"hi\\u0020\"}")));
        assertEquals("{ \"x\" : \"\u0E01\u2702\uF900\"}", JSON.serialize(JSON.parse("{ \"x\" : \"\\u0E01\\u2702\\uF900\"}")));
        assertEquals("{ \"x\" : \"foo bar\"}", JSON.serialize(JSON.parse("{ \"x\" : \"foo\\u0020bar\"}")));
    }

    @Test
    public void testBin() {
        byte[] b = {'a', 'b', 0, 'd'};
        DBObject obj = BasicDBObjectBuilder.start().add("b", b).get();
        assertEquals("{ \"b\" : <Binary Data>}", JSON.serialize(obj));
    }


    @Test
    public void testErrors() {
        boolean threw = false;
        try {
            JSON.parse("{\"x\" : \"");
        } catch (JSONParseException e) {
            threw = true;
        }
        assertEquals(true, threw);
        threw = false;
        try {
            JSON.parse("{\"x\" : \"\\");
        } catch (JSONParseException e) {
            threw = true;
        }
        assertEquals(true, threw);
        threw = false;
        try {
            JSON.parse("{\"x\" : 5.2");
        } catch (JSONParseException e) {
            threw = true;
        }
        assertEquals(true, threw);
        threw = false;
        try {
            JSON.parse("{\"x\" : 5");
        } catch (JSONParseException e) {
            threw = true;
        }
        assertEquals(true, threw);
        threw = false;
        try {
            JSON.parse("{\"x\" : 5,");
        } catch (JSONParseException e) {
            threw = true;
        }
        assertEquals(true, threw);
        threw = false;
    }

    @Test
    public void testBasic() {
        assertEquals("{ }", JSON.serialize(JSON.parse("{}")));
        assertEquals(null, JSON.parse(""));
        assertEquals(null, JSON.parse("     "));
        assertEquals(null, JSON.parse(null));

        boolean threw = false;
        try {
            JSON.parse("{");
        } catch (JSONParseException e) {
            threw = true;
        }
        assertEquals(true, threw);
        threw = false;

        try {
            JSON.parse("}");
        } catch (JSONParseException e) {
            threw = true;
        }
        assertEquals(true, threw);
        threw = false;

        try {
            JSON.parse("{{}");
        } catch (JSONParseException e) {
            threw = true;
        }
        assertEquals(true, threw);
        threw = false;

        try {
            JSON.parse("4");
        } catch (JSONParseException e) {
            threw = true;
        }
        assertEquals(false, threw);
        threw = false;

        assertEquals(JSON.parse("4"), 4);
    }

    @Test
    public void testNumbers2() {
        DBObject x = new BasicDBObject("x", 123);
        assertEquals(JSON.parse(x.toString()), x);

        x = new BasicDBObject("x", 123123123123L);
        assertEquals(JSON.parse(x.toString()), x);

        x = new BasicDBObject("x", 123123123);
        assertEquals(JSON.parse(x.toString()), x);
    }

    private void escapeChar(final String s) {
        String thingy = "va" + s + "lue";
        DBObject x = new BasicDBObject("name", thingy);
        x = (DBObject) JSON.parse(x.toString());
        assertEquals(x.get("name"), thingy);

        thingy = "va" + s + s + s + "lue" + s;
        x = new BasicDBObject("name", thingy);
        x = (DBObject) JSON.parse(x.toString());
        assertEquals(x.get("name"), thingy);
    }


    @Test
    public void testEscape1() {
        String raw = "a\nb";

        DBObject x = new BasicDBObject("x", raw);
        assertEquals("\"a\\nb\"", JSON.serialize(raw));
        assertEquals(x, JSON.parse(x.toString()));
        assertEquals(raw, ((DBObject) JSON.parse(x.toString())).get("x"));

        x = new BasicDBObject("x", "a\nb\bc\td\re");
        assertEquals(x, JSON.parse(x.toString()));


        String thingy = "va\"lue";
        x = new BasicDBObject("name", thingy);
        x = (DBObject) JSON.parse(x.toString());
        assertEquals(thingy, x.get("name"));

        thingy = "va\\lue";
        x = new BasicDBObject("name", thingy);
        x = (DBObject) JSON.parse(x.toString());
        assertEquals(thingy, x.get("name"));

        assertEquals("va/lue", JSON.parse("\"va\\/lue\""));
        assertEquals("value", JSON.parse("\"va\\lue\""));
        assertEquals("va\\lue", JSON.parse("\"va\\\\lue\""));

        escapeChar("\t");
        escapeChar("\b");
        escapeChar("\n");
        escapeChar("\r");
        escapeChar("\'");
        escapeChar("\"");
        escapeChar("\\");
    }

    // This is not correct behavior, but adding the test here to document it.  It should probably throw a JSONParseException due to the
    // illegal escape of \m.
    @Test
    public void testIllegalEscape() {
        DBObject obj = (DBObject) JSON.parse("{ 'x' : '\\m' }");
        assertEquals("m", obj.get("x"));
    }

    @Test
    public void testPattern() {
        String x = "^Hello$";
        String serializedPattern =
            "{ \"$regex\" : \"" + x + "\" , \"$options\" : \"" + "i\"}";

        Pattern pattern = Pattern.compile(x, Pattern.CASE_INSENSITIVE);
        assertEquals(serializedPattern, JSON.serialize(pattern));

        BasicDBObject a = new BasicDBObject("x", pattern);
        assertEquals("{ \"x\" : " + serializedPattern + "}", a.toString());

        DBObject b = (DBObject) JSON.parse(a.toString());
        assertEquals(Pattern.class, b.get("x").getClass());
        assertEquals(b.toString(), a.toString());
    }

    @Test
    public void testRegexNoOptions() {
        String x = "^Hello$";
        String serializedPattern =
            "{ \"$regex\" : \"" + x + "\"}";

        Pattern pattern = Pattern.compile(x);
        assertEquals(JSON.serialize(pattern), serializedPattern);

        BasicDBObject a = new BasicDBObject("x", pattern);
        assertEquals(a.toString(), "{ \"x\" : " + serializedPattern + "}");

        DBObject b = (DBObject) JSON.parse(a.toString());
        assertEquals(Pattern.class, b.get("x").getClass());
        assertEquals(b.toString(), a.toString());
    }

    @Test
    public void testObjectId() {
        ObjectId oid = new ObjectId(new Date());

        String serialized = JSON.serialize(oid);
        assertEquals("{ \"$oid\" : \"" + oid + "\"}", serialized);

        ObjectId oid2 = (ObjectId) JSON.parse(serialized);
        assertEquals(oid, oid2);
    }

    @Test
    public void testDate() {
        Date d = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
        format.setCalendar(new GregorianCalendar(new SimpleTimeZone(0, "GMT")));
        String formattedDate = format.format(d);

        String serialized = JSON.serialize(d);
        assertEquals("{ \"$date\" : \"" + formattedDate + "\"}", serialized);

        Date d2 = (Date) JSON.parse(serialized);
        assertEquals(d2.toString(), d.toString());
        assertTrue(d.equals(d2));
    }

    @Test
    public void testJSONEncoding() throws ParseException {
        String json = "{ 'str' : 'asdfasd' , 'long' : 123123123123 , 'int' : 5 , 'float' : 0.4 , 'bool' : false , "
                      + "'date' : { '$date' : '2011-05-18T18:56:00Z'} , 'pat' : { '$regex' : '.*' , '$options' : ''} , "
                      + "'oid' : { '$oid' : '4d83ab3ea39562db9c1ae2ae'} , 'ref' : { '$ref' : 'test.test' , "
                      + "'$id' : { '$oid' : '4d83ab59a39562db9c1ae2af'}} , 'code' : { '$code' : 'asdfdsa'} , "
                      + "'codews' : { '$code' : 'ggggg' , "
                      + "'$scope' : { }} , 'ts' : { '$ts' : 1300474885 , '$inc' : 10} , 'null' :  null, "
                      + "'uuid' : { '$uuid' : '60f65152-6d4a-4f11-9c9b-590b575da7b5' }}";
        BasicDBObject a = (BasicDBObject) JSON.parse(json);
        assertEquals("asdfasd", a.get("str"));
        assertEquals(5, a.get("int"));
        assertEquals(123123123123L, a.get("long"));
        assertEquals(0.4d, a.get("float"));
        assertEquals(false, a.get("bool"));
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        format.setCalendar(new GregorianCalendar(new SimpleTimeZone(0, "GMT")));
        assertEquals(format.parse("2011-05-18T18:56:00Z"), a.get("date"));
        Pattern pat = (Pattern) a.get("pat");
        Pattern pat2 = Pattern.compile(".*", BSON.regexFlags(""));
        assertEquals(pat.pattern(), pat2.pattern());
        assertEquals(pat.flags(), pat2.flags());
        ObjectId oid = (ObjectId) a.get("oid");
        assertEquals(new ObjectId("4d83ab3ea39562db9c1ae2ae"), oid);
        //        DBRef ref = (DBRef) a.get("ref");
        //        assertEquals(new DBRef(null, "test.test", new ObjectId("4d83ab59a39562db9c1ae2af")), ref);
        assertEquals(new Code("asdfdsa"), a.get("code"));
        assertEquals(new CodeWScope("ggggg", new BasicBSONObject()), a.get("codews"));
        assertEquals(new BSONTimestamp(1300474885, 10), a.get("ts"));
        assertEquals(UUID.fromString("60f65152-6d4a-4f11-9c9b-590b575da7b5"), a.get("uuid"));
        String json2 = JSON.serialize(a);
        BasicDBObject b = (BasicDBObject) JSON.parse(json2);
        assertEquals(JSON.serialize(b), JSON.serialize(b));
    }

}
