/*
 * Copyright (c) 2008-2016 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 org.bson.json;

import org.bson.BsonBinary;
import org.bson.BsonDbPointer;
import org.bson.BsonInvalidOperationException;
import org.bson.BsonRegularExpression;
import org.bson.BsonTimestamp;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId;
import org.junit.Before;
import org.junit.Test;

import java.io.StringWriter;
import java.util.Date;
import java.util.List;

import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;

@SuppressWarnings("unchecked")
public class JsonWriterTest {
    private StringWriter stringWriter;
    private JsonWriter writer;

    @Before
    public void before() {
        stringWriter = new StringWriter();
        writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().build());
    }

    private static class TestData<T> {
        private final T value;
        private final String expected;

        TestData(final T value, final String expected) {
            this.value = value;
            this.expected = expected;
        }
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowExceptionForBooleanWhenWritingBeforeStartingDocument() {
        writer.writeBoolean("b1", true);
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowExceptionForNameWhenWritingBeforeStartingDocument() {
        writer.writeName("name");
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowExceptionForStringWhenStateIsValue() {
        writer.writeStartDocument();
        writer.writeString("SomeString");
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowExceptionWhenEndingAnArrayWhenStateIsValue() {
        writer.writeStartDocument();
        writer.writeEndArray();
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowExceptionWhenWritingASecondName() {
        writer.writeStartDocument();
        writer.writeName("f1");
        writer.writeName("i2");
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowExceptionWhenEndingADocumentBeforeValueIsWritten() {
        writer.writeStartDocument();
        writer.writeName("f1");
        writer.writeEndDocument();
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnExceptionWhenTryingToWriteASecondValue() {
        writer.writeDouble(100);
        writer.writeString("i2");
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnExceptionWhenTryingToWriteJavaScript() {
        writer.writeDouble(100);
        writer.writeJavaScript("var i");
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnExceptionWhenWritingANameInAnArray() {
        writer.writeStartDocument();
        writer.writeStartArray("f2");
        writer.writeName("i3");
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnExceptionWhenEndingDocumentInTheMiddleOfWritingAnArray() {
        writer.writeStartDocument();
        writer.writeStartArray("f2");
        writer.writeEndDocument();
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnExceptionWhenEndingAnArrayInASubDocument() {
        writer.writeStartDocument();
        writer.writeStartArray("f2");
        writer.writeStartDocument();
        writer.writeEndArray();
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnExceptionWhenWritingANameInAnArrayEvenWhenSubDocumentExistsInArray() {
        writer.writeStartDocument();
        writer.writeStartArray("f2");
        writer.writeStartDocument();
        writer.writeEndDocument();
        writer.writeName("i3");
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnExceptionWhenAttemptingToEndAnArrayThatWasNotStarted() {
        writer.writeStartDocument();
        writer.writeStartArray("f2");
        writer.writeEndArray();
        writer.writeEndArray();
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnErrorIfTryingToWriteNameIntoAJavascriptScope() {
        writer.writeStartDocument();
        writer.writeJavaScriptWithScope("js1", "var i = 1");

        writer.writeName("b1");
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnErrorIfTryingToWriteValueIntoAJavascriptScope() {
        writer.writeStartDocument();
        writer.writeJavaScriptWithScope("js1", "var i = 1");

        writer.writeBinaryData(new BsonBinary(new byte[]{0, 0, 1, 0}));
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnErrorIfTryingToWriteArrayIntoAJavascriptScope() {
        writer.writeStartDocument();
        writer.writeJavaScriptWithScope("js1", "var i = 1");

        writer.writeStartArray();
    }

    @Test(expected = BsonInvalidOperationException.class)
    public void shouldThrowAnErrorIfTryingToWriteEndDocumentIntoAJavascriptScope() {
        writer.writeStartDocument();
        writer.writeJavaScriptWithScope("js1", "var i = 1");

        writer.writeEndDocument();
    }

    @Test
    public void testEmptyDocument() {
        writer.writeStartDocument();
        writer.writeEndDocument();
        String expected = "{ }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testSingleElementDocument() {
        writer.writeStartDocument();
        writer.writeName("s");
        writer.writeString("str");
        writer.writeEndDocument();
        String expected = "{ \"s\" : \"str\" }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testTwoElementDocument() {
        writer.writeStartDocument();
        writer.writeName("s");
        writer.writeString("str");
        writer.writeName("d");
        writer.writeString("str2");
        writer.writeEndDocument();
        String expected = "{ \"s\" : \"str\", \"d\" : \"str2\" }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testNestedDocument() {
        writer.writeStartDocument();
        writer.writeName("doc");
        writer.writeStartDocument();
        writer.writeName("doc");
        writer.writeStartDocument();
        writer.writeName("s");
        writer.writeString("str");
        writer.writeEndDocument();
        writer.writeEndDocument();
        writer.writeEndDocument();
        String expected = "{ \"doc\" : { \"doc\" : { \"s\" : \"str\" } } }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testSingleString() {
        writer.writeStartDocument();
        writer.writeString("abc", "xyz");
        writer.writeEndDocument();
        String expected = "{ \"abc\" : \"xyz\" }";
        assertEquals(expected, stringWriter.toString());
    }


    @Test
    public void testBoolean() {
        writer.writeStartDocument();
        writer.writeBoolean("abc", true);
        writer.writeEndDocument();
        String expected = "{ \"abc\" : true }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testDouble() {
        List<TestData<Double>> tests = asList(new TestData<Double>(0.0, "0.0"), new TestData<Double>(0.0005, "5.0E-4"),
                new TestData<Double>(0.5, "0.5"), new TestData<Double>(1.0, "1.0"),
                new TestData<Double>(1.5, "1.5"), new TestData<Double>(1.5E+40, "1.5E40"),
                new TestData<Double>(1.5E-40, "1.5E-40"),
                new TestData<Double>(1234567890.1234568E+123, "1.2345678901234568E132"),
                new TestData<Double>(Double.MAX_VALUE, "1.7976931348623157E308"),
                new TestData<Double>(Double.MIN_VALUE, "4.9E-324"),

                new TestData<Double>(-0.0005, "-5.0E-4"),
                new TestData<Double>(-0.5, "-0.5"),
                new TestData<Double>(-1.0, "-1.0"),
                new TestData<Double>(-1.5, "-1.5"),
                new TestData<Double>(-1.5E+40, "-1.5E40"),
                new TestData<Double>(-1.5E-40, "-1.5E-40"),
                new TestData<Double>(-1234567890.1234568E+123, "-1.2345678901234568E132"),

                new TestData<Double>(Double.NaN, "NaN"),
                new TestData<Double>(Double.NEGATIVE_INFINITY, "-Infinity"),
                new TestData<Double>(Double.POSITIVE_INFINITY, "Infinity"));
        for (final TestData<Double> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.EXTENDED).build());
            writer.writeStartDocument();
            writer.writeDouble("d", cur.value);
            writer.writeEndDocument();
            String expected = "{ \"d\" : { \"$numberDouble\" : \"" + cur.expected + "\" } }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    public void testInt64Shell() {
        List<TestData<Long>> tests = asList(new TestData<Long>(Long.MIN_VALUE, "NumberLong(\"-9223372036854775808\")"),
                new TestData<Long>(Integer.MIN_VALUE - 1L, "NumberLong(\"-2147483649\")"),
                new TestData<Long>(Integer.MIN_VALUE + 0L, "NumberLong(-2147483648)"),
                new TestData<Long>(0L, "NumberLong(0)"),
                new TestData<Long>(Integer.MAX_VALUE + 0L, "NumberLong(2147483647)"),
                new TestData<Long>(Integer.MAX_VALUE + 1L, "NumberLong(\"2147483648\")"),
                new TestData<Long>(Long.MAX_VALUE, "NumberLong(\"9223372036854775807\")"));
        for (final TestData<Long> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.SHELL).build());
            writer.writeStartDocument();
            writer.writeInt64("l", cur.value);
            writer.writeEndDocument();
            String expected = "{ \"l\" : " + cur.expected + " }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    @SuppressWarnings("deprecation")
    public void testInt64Strict() {
        List<TestData<Long>> tests = asList(new TestData<Long>(Long.MIN_VALUE, "-9223372036854775808"),
                new TestData<Long>(Integer.MIN_VALUE - 1L, "-2147483649"),
                new TestData<Long>(Integer.MIN_VALUE - 0L, "-2147483648"),
                new TestData<Long>(0L, "0"),
                new TestData<Long>(Integer.MAX_VALUE + 0L, "2147483647"),
                new TestData<Long>(Integer.MAX_VALUE + 1L, "2147483648"),
                new TestData<Long>(Long.MAX_VALUE, "9223372036854775807"));

        for (final TestData<Long> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.STRICT).build());
            writer.writeStartDocument();
            writer.writeInt64("l", cur.value);
            writer.writeEndDocument();
            String expected = "{ \"l\" : { \"$numberLong\" : \"" + cur.expected + "\" } }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    public void testDecimal128SShell() {
        List<TestData<Decimal128>> tests = asList(
                new TestData<Decimal128>(Decimal128.parse("1.0"), "1.0"),
                new TestData<Decimal128>(Decimal128.POSITIVE_INFINITY, Decimal128.POSITIVE_INFINITY.toString()));


        for (final TestData<Decimal128> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.SHELL).build());
            writer.writeStartDocument();
            writer.writeDecimal128("d", cur.value);
            writer.writeEndDocument();
            String expected = "{ \"d\" : NumberDecimal(\"" + cur.expected + "\") }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    @SuppressWarnings("deprecation")
    public void testDecimal128Strict() {
        List<TestData<Decimal128>> tests = asList(
                new TestData<Decimal128>(Decimal128.parse("1.0"), "1.0"),
                new TestData<Decimal128>(Decimal128.POSITIVE_INFINITY, Decimal128.POSITIVE_INFINITY.toString()));


        for (final TestData<Decimal128> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.STRICT).build());
            writer.writeStartDocument();
            writer.writeDecimal128("d", cur.value);
            writer.writeEndDocument();
            String expected = "{ \"d\" : { \"$numberDecimal\" : \"" + cur.expected + "\" } }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    public void testArray() {
        writer.writeStartDocument();
        writer.writeStartArray("array");
        writer.writeInt32(1);
        writer.writeInt32(2);
        writer.writeInt32(3);
        writer.writeEndArray();
        writer.writeEndDocument();
        String expected = "{ \"array\" : [1, 2, 3] }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    @SuppressWarnings("deprecation")
    public void testBinaryStrict() {
        List<TestData<BsonBinary>> tests = asList(new TestData<BsonBinary>(new BsonBinary(new byte[0]),
                                                                   "{ \"$binary\" : \"\", "
                                                                   + "\"$type\" : \"00\" }"),
                                              new TestData<BsonBinary>(new BsonBinary(new byte[]{1}),
                                                                   "{ \"$binary\" : \"AQ==\", "
                                                                   + "\"$type\" : \"00\" }"),
                                              new TestData<BsonBinary>(new BsonBinary(new byte[]{1, 2}),
                                                                   "{ \"$binary\" : \"AQI=\", "
                                                                   + "\"$type\" : \"00\" }"),
                                              new TestData<BsonBinary>(new BsonBinary(new byte[]{1, 2, 3}),
                                                                   "{ \"$binary\" : \"AQID\", "
                                                                   + "\"$type\" : \"00\" }"),
                                              new TestData<BsonBinary>(new BsonBinary((byte) 0x80, new byte[]{1, 2, 3}),
                                                                   "{ \"$binary\" : \"AQID\", "
                                                                   + "\"$type\" : \"80\" }"));
        for (final TestData<BsonBinary> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.STRICT).build());
            writer.writeStartDocument();
            writer.writeBinaryData("binary", cur.value);
            writer.writeEndDocument();
            String expected = "{ \"binary\" : " + cur.expected + " }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    public void testBinaryShell() {
        List<TestData<BsonBinary>> tests = asList(new TestData<BsonBinary>(new BsonBinary(new byte[0]), "new BinData(0, \"\")"),
                new TestData<BsonBinary>(new BsonBinary(new byte[]{1}), "new BinData(0, \"AQ==\")"),
                new TestData<BsonBinary>(new BsonBinary(new byte[]{1, 2}), "new BinData(0, \"AQI=\")"),
                new TestData<BsonBinary>(new BsonBinary(new byte[]{1, 2, 3}), "new BinData(0, \"AQID\")"),
                new TestData<BsonBinary>(new BsonBinary((byte) 0x80, new byte[]{1, 2, 3}),
                                                "new BinData(128, \"AQID\")"));
        for (final TestData<BsonBinary> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.SHELL).build());
            writer.writeStartDocument();
            writer.writeBinaryData("binary", cur.value);
            writer.writeEndDocument();
            String expected = "{ \"binary\" : " + cur.expected + " }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    @SuppressWarnings("deprecation")
    public void testDateTimeStrict() {
        List<TestData<Date>> tests = asList(new TestData<Date>(new Date(0), "{ \"$date\" : 0 }"),
                new TestData<Date>(new Date(Long.MAX_VALUE), "{ \"$date\" : 9223372036854775807 }"),
                new TestData<Date>(new Date(Long.MIN_VALUE), "{ \"$date\" : -9223372036854775808 }"));
        for (final TestData<Date> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.STRICT).build());
            writer.writeStartDocument();
            writer.writeDateTime("date", cur.value.getTime());
            writer.writeEndDocument();
            String expected = "{ \"date\" : " + cur.expected + " }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    public void testDateTimeShell() {
        List<TestData<Date>> tests = asList(new TestData<Date>(new Date(0), "ISODate(\"1970-01-01T00:00:00.000Z\")"),
                new TestData<Date>(new Date(1), "ISODate(\"1970-01-01T00:00:00.001Z\")"),
                new TestData<Date>(new Date(-1), "ISODate(\"1969-12-31T23:59:59.999Z\")"),
                new TestData<Date>(new Date(Long.MAX_VALUE), "new Date(9223372036854775807)"),
                new TestData<Date>(new Date(Long.MIN_VALUE), "new Date(-9223372036854775808)"));
        for (final TestData<Date> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.SHELL).build());
            writer.writeStartDocument();
            writer.writeDateTime("date", cur.value.getTime());
            writer.writeEndDocument();
            String expected = "{ \"date\" : " + cur.expected + " }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    public void testJavaScript() {
        writer.writeStartDocument();
        writer.writeJavaScript("f", "function f() { return 1; }");
        writer.writeEndDocument();
        String expected = "{ \"f\" : { \"$code\" : \"function f() { return 1; }\" } }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testJavaScriptWithScope() {
        writer.writeStartDocument();
        writer.writeJavaScriptWithScope("f", "function f() { return n; }");
        writer.writeStartDocument();
        writer.writeInt32("n", 1);
        writer.writeEndDocument();
        writer.writeEndDocument();
        String expected =
                "{ \"f\" : { \"$code\" : \"function f() { return n; }\", " + "\"$scope\" : { \"n\" : 1 } } }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testMaxKeyStrict() {
        writer.writeStartDocument();
        writer.writeMaxKey("maxkey");
        writer.writeEndDocument();
        String expected = "{ \"maxkey\" : { \"$maxKey\" : 1 } }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testMinKeyStrict() {
        writer.writeStartDocument();
        writer.writeMinKey("minkey");
        writer.writeEndDocument();
        String expected = "{ \"minkey\" : { \"$minKey\" : 1 } }";
        assertEquals(expected, stringWriter.toString());
    }


    @Test
    public void testMaxKeyShell() {
        writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.SHELL).build());
        writer.writeStartDocument();
        writer.writeMaxKey("maxkey");
        writer.writeEndDocument();
        String expected = "{ \"maxkey\" : MaxKey }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testMinKeyShell() {
        writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.SHELL).build());
        writer.writeStartDocument();
        writer.writeMinKey("minkey");
        writer.writeEndDocument();
        String expected = "{ \"minkey\" : MinKey }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testNull() {
        writer.writeStartDocument();
        writer.writeNull("null");
        writer.writeEndDocument();
        String expected = "{ \"null\" : null }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testObjectIdShell() {
        writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.SHELL).build());
        ObjectId objectId = new ObjectId("4d0ce088e447ad08b4721a37");

        writer.writeStartDocument();
        writer.writeObjectId("_id", objectId);
        writer.writeEndDocument();

        String expected = "{ \"_id\" : ObjectId(\"4d0ce088e447ad08b4721a37\") }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testObjectIdStrict() {
        ObjectId objectId = new ObjectId("4d0ce088e447ad08b4721a37");

        writer.writeStartDocument();
        writer.writeObjectId("_id", objectId);
        writer.writeEndDocument();

        String expected = "{ \"_id\" : { \"$oid\" : \"4d0ce088e447ad08b4721a37\" } }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testRegularExpressionShell() {
        List<TestData<BsonRegularExpression>> tests;
        tests = asList(new TestData<BsonRegularExpression>(new BsonRegularExpression(""), "/(?:)/"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a"), "/a/"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a/b"), "/a\\/b/"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a\\b"), "/a\\b/"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a", "i"), "/a/i"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a", "m"), "/a/m"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a", "x"), "/a/x"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a", "s"), "/a/s"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a", "imxs"), "/a/imsx"));
        for (final TestData<BsonRegularExpression> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.SHELL).build());
            writer.writeStartDocument();
            writer.writeRegularExpression("regex", cur.value);
            writer.writeEndDocument();
            String expected = "{ \"regex\" : " + cur.expected + " }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    @SuppressWarnings("deprecation")
    public void testRegularExpressionStrict() {
        List<TestData<BsonRegularExpression>> tests;
        tests = asList(new TestData<BsonRegularExpression>(new BsonRegularExpression(""), "{ \"$regex\" : \"\", "
                                                                                  + "\"$options\" : \"\" "
                                                                                  + "}"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a"), "{ \"$regex\" : \"a\","
                                                                                   + " \"$options\" : \"\" "
                                                                                   + "}"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a/b"), "{ \"$regex\" : "
                                                                                     + "\"a/b\", "
                                                                                     + "\"$options\" : \"\" "
                                                                                     + "}"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a\\b"), "{ \"$regex\" : "
                                                                                      + "\"a\\\\b\", "
                                                                                      + "\"$options\" : \"\" "
                                                                                      + "}"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a", "i"), "{ \"$regex\" : \"a\","
                                                                                        + " \"$options\" : \"i\""
                                                                                        + " }"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a", "m"), "{ \"$regex\" : \"a\","
                                                                                        + " \"$options\" : \"m\""
                                                                                        + " }"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a", "x"), "{ \"$regex\" : \"a\","
                                                                                        + " \"$options\" : \"x\""
                                                                                        + " }"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a", "s"), "{ \"$regex\" : \"a\","
                                                                                        + " \"$options\" : \"s\""
                                                                                        + " }"),
                       new TestData<BsonRegularExpression>(new BsonRegularExpression("a", "imxs"),
                                                       "{ \"$regex\" : \"a\"," + " \"$options\" : \"imsx\" }"));
        for (final TestData<BsonRegularExpression> cur : tests) {
            stringWriter = new StringWriter();
            writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.STRICT).build());
            writer.writeStartDocument();
            writer.writeRegularExpression("regex", cur.value);
            writer.writeEndDocument();
            String expected = "{ \"regex\" : " + cur.expected + " }";
            assertEquals(expected, stringWriter.toString());
        }
    }

    @Test
    public void testSymbol() {
        writer.writeStartDocument();
        writer.writeSymbol("symbol", "name");
        writer.writeEndDocument();
        String expected = "{ \"symbol\" : { \"$symbol\" : \"name\" } }";
        assertEquals(expected, stringWriter.toString());
    }

    //
    @Test
    public void testTimestampStrict() {
        writer.writeStartDocument();
        writer.writeTimestamp("timestamp", new BsonTimestamp(1000, 1));
        writer.writeEndDocument();
        String expected = "{ \"timestamp\" : { \"$timestamp\" : { \"t\" : 1000, \"i\" : 1 } } }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testTimestampShell() {
        writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.SHELL).build());
        writer.writeStartDocument();
        writer.writeTimestamp("timestamp", new BsonTimestamp(1000, 1));
        writer.writeEndDocument();
        String expected = "{ \"timestamp\" : Timestamp(1000, 1) }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    @SuppressWarnings("deprecation")
    public void testUndefinedStrict() {
        writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.STRICT).build());
        writer.writeStartDocument();
        writer.writeUndefined("undefined");
        writer.writeEndDocument();
        String expected = "{ \"undefined\" : { \"$undefined\" : true } }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testUndefinedShell() {
        writer = new JsonWriter(stringWriter, JsonWriterSettings.builder().outputMode(JsonMode.SHELL).build());
        writer.writeStartDocument();
        writer.writeUndefined("undefined");
        writer.writeEndDocument();
        String expected = "{ \"undefined\" : undefined }";
        assertEquals(expected, stringWriter.toString());
    }

    @Test
    public void testDBPointer() {
        writer.writeStartDocument();
        writer.writeDBPointer("dbPointer", new BsonDbPointer("my.test", new ObjectId("4d0ce088e447ad08b4721a37")));
        writer.writeEndDocument();
        String expected = "{ \"dbPointer\" : { \"$ref\" : \"my.test\", \"$id\" : { \"$oid\" : \"4d0ce088e447ad08b4721a37\" } } }";
        assertEquals(expected, stringWriter.toString());
    }
}
