// Copyright 2014 Cognitect. All Rights Reserved.
//
// 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.cognitect.transit;

import com.cognitect.transit.impl.*;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.apache.commons.codec.binary.Base64;

import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
import java.util.*;

public class TransitTest extends TestCase {

    public TransitTest(String testName) {
        super(testName);
    }

    public static Test suite() {
        return new TestSuite(TransitTest.class);
    }

    // Reading

    public Reader reader(String s)  {
        try {
            InputStream in = new ByteArrayInputStream(s.getBytes());
            return TransitFactory.reader(TransitFactory.Format.JSON, in);
        } catch (Throwable e) { throw new RuntimeException(e); }
    }

    public Reader reader(String s, Map<String, ReadHandler<?, ?>> customHandlers) {
        try {
            InputStream in = new ByteArrayInputStream(s.getBytes());
            return TransitFactory.reader(TransitFactory.Format.JSON, in, customHandlers);
        } catch (Throwable e) { throw new RuntimeException(e); }
    }

    public Reader reader(String s, DefaultReadHandler defaultReadHandler) {
        try {
            InputStream in = new ByteArrayInputStream(s.getBytes());
            return TransitFactory.reader(TransitFactory.Format.JSON, in, defaultReadHandler);
        } catch (Throwable e) { throw new RuntimeException(e); }
    }

    public void testReadString() throws IOException {

        assertEquals("foo", reader("\"foo\"").read());
        assertEquals("~foo", reader("\"~~foo\"").read());
        assertEquals("`foo", reader("\"~`foo\"").read());
        assertEquals("foo", ((Tag)reader("\"~#foo\"").read()).getValue());
        assertEquals("^foo", reader("\"~^foo\"").read());
    }

    public void testReadBoolean() throws IOException {

        assertTrue((Boolean)reader("\"~?t\"").read());
        assertFalse((Boolean)reader("\"~?f\"").read());

        Map m = (Map)reader("{\"~?t\":1,\"~?f\":2}").read();
        assertEquals(1L, m.get(true));
        assertEquals(2L, m.get(false));
    }

    public void testReadNull() throws IOException {

        assertNull(reader("\"~_\"").read());
    }

    public void testReadKeyword() throws IOException {

        Object v = reader("\"~:foo\"").read();
        assertEquals(":foo", v.toString());

        List v2 = (List)reader("[\"~:foo\",\"^"+(char)WriteCache.BASE_CHAR_IDX+"\",\"^"+(char)WriteCache.BASE_CHAR_IDX+"\"]").read();
        assertEquals(":foo", v2.get(0).toString());
        assertEquals(":foo", v2.get(1).toString());
        assertEquals(":foo", v2.get(2).toString());
    }

    public void testReadInteger() throws IOException {

        Reader r = reader("\"~i42\"");
        assertEquals(42L, (long) r.read());
        r = reader("\"~n4256768765123454321897654321234567\"");
        assertEquals(0, (new BigInteger("4256768765123454321897654321234567")).compareTo(
                          (BigInteger)r.read()));
    }

    public void testReadDouble() throws IOException {

        assertEquals(new Double("42.5"), reader("\"~d42.5\"").read());
    }

    public void testReadSpecialNumbers() throws IOException {
        assertEquals(Double.NaN, reader("\"~zNaN\"").read());
        assertEquals(Double.POSITIVE_INFINITY, reader("\"~zINF\"").read());
        assertEquals(Double.NEGATIVE_INFINITY, reader("\"~z-INF\"").read());
    }

    public void testReadBigDecimal() throws IOException {

        assertEquals(0, (new BigDecimal("42.5")).compareTo(
                          (BigDecimal)reader("\"~f42.5\"").read()));
    }

    private long readTimeString(String timeString) throws IOException {
        return ((Date)reader("\"~t" + timeString + "\"").read()).getTime();
    }

    private SimpleDateFormat formatter(String formatString) {

        SimpleDateFormat df = new SimpleDateFormat(formatString);
        df.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
        return df;
    }

    private void assertReadsFormat(String formatString) throws Exception {

        Date d = new Date();
        SimpleDateFormat df = formatter(formatString);
        String ds = df.format(d);
        assertEquals(df.parse(ds).getTime(), readTimeString(ds));
    }

    public void testReadTime() throws Exception {

        Date d = new Date();
        long t = d.getTime();
        String timeString = JsonParser.getDateTimeFormat().format(d);

        assertEquals(t, readTimeString(timeString));

        assertEquals(t, ((Date)reader("{\"~#m\": " + t + "}").read()).getTime());

        assertReadsFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
        assertReadsFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        assertReadsFormat("yyyy-MM-dd'T'HH:mm:ss.SSS-00:00");
    }

    public void testReadUUID() throws IOException {

        UUID uuid = UUID.randomUUID();
        long hi64 = uuid.getMostSignificantBits();
        long lo64 = uuid.getLeastSignificantBits();

        assertEquals(0, uuid.compareTo((UUID)reader("\"~u" + uuid.toString() + "\"").read()));
        assertEquals(0, uuid.compareTo((UUID)reader("{\"~#u\": [" + hi64 + ", " + lo64 + "]}").read()));
    }

    public void testReadURI() throws IOException {

        URI uri = TransitFactory.uri("http://www.foo.com");

        assertEquals(0, uri.compareTo((URI)reader("\"~rhttp://www.foo.com\"").read()));
    }

    public void testReadSymbol() throws IOException {

        Reader r = reader("\"~$foo\"");
        Object v = r.read();
        assertEquals("foo", v.toString());
    }

    public void testReadCharacter() throws IOException {
        assertEquals('f', (char) reader("\"~cf\"").read());
    }

    public void testReadBinary() throws IOException {

        byte[] bytes = "foobarbaz".getBytes();
        byte[] encodedBytes = Base64.encodeBase64(bytes);
        byte[] decoded = (byte[])reader("\"~b" + new String(encodedBytes) + "\"").read();

        assertEquals(bytes.length, decoded.length);

        boolean same = true;
        for(int i=0;i<bytes.length;i++) {
            if(bytes[i]!=decoded[i])
                same = false;
        }

        assertTrue(same);
    }

    public void testReadUnknown() throws IOException {

        assertEquals(TransitFactory.taggedValue("j", "foo"), reader("\"~jfoo\"").read());
        List l = Arrays.asList(1L, 2L);
        assertEquals(TransitFactory.taggedValue("point", l), reader("{\"~#point\":[1,2]}").read());
    }

    public void testReadArray() throws IOException {

        List l = (List)reader("[1, 2, 3]").read();

        assertTrue(l instanceof ArrayList);
        assertEquals(3, l.size());

        assertEquals(1L, l.get(0));
        assertEquals(2L, l.get(1));
        assertEquals(3L, l.get(2));
    }

    public void testReadArrayWithNested() throws IOException {

        Date d = new Date();
        String t = JsonParser.getDateTimeFormat().format(d);

        List l = (List)reader("[\"~:foo\", \"~t" + t + "\", \"~?t\"]").read();

        assertEquals(3, l.size());

        assertEquals(":foo", l.get(0).toString());
        assertEquals(d.getTime(), ((Date)l.get(1)).getTime());
        assertTrue((Boolean) l.get(2));
    }

    public void testReadMap() throws IOException {

        Map m = (Map)reader("{\"a\": 2, \"b\": 4}").read();

        assertEquals(2, m.size());

        assertEquals(2L, m.get("a"));
        assertEquals(4L, m.get("b"));
    }

    public void testReadMapWithNested() throws IOException {

        String uuid = UUID.randomUUID().toString();

        Map m = (Map)reader("{\"a\": \"~:foo\", \"b\": \"~u" + uuid + "\"}").read();

        assertEquals(2, m.size());

        assertEquals(":foo", m.get("a").toString());
        assertEquals(uuid, m.get("b").toString());
    }

    public void testReadSet() throws IOException {

        Set s = (Set)reader("{\"~#set\": [1, 2, 3]}").read();

        assertEquals(3, s.size());

        assertTrue(s.contains(1L));
        assertTrue(s.contains(2L));
        assertTrue(s.contains(3L));
    }

    public void testReadList() throws IOException {

        List l = (List)reader("{\"~#list\": [1, 2, 3]}").read();

        assertTrue(l instanceof LinkedList);
        assertEquals(3, l.size());

        assertEquals(1L, l.get(0));
        assertEquals(2L, l.get(1));
        assertEquals(3L, l.get(2));
    }

    public void testReadRatio() throws IOException {

        Ratio r = (Ratio)reader("{\"~#ratio\": [\"~n1\",\"~n2\"]}").read();

        assertEquals(BigInteger.valueOf(1), r.getNumerator());
        assertEquals(BigInteger.valueOf(2), r.getDenominator());
        assertEquals(0.5d, r.getValue().doubleValue(), 0.01d);
    }

    public void testReadCmap() throws IOException {

        Map m = reader("{\"~#cmap\": [{\"~#ratio\":[\"~n1\",\"~n2\"]},1,{\"~#list\":[1,2,3]},2]}").read();

        assertEquals(2, m.size());

        Iterator<Map.Entry> i = m.entrySet().iterator();
        while(i.hasNext()) {
            Map.Entry e = i.next();
            if((Long)e.getValue() == 1L) {
                Ratio r = (Ratio)e.getKey();
                assertEquals(BigInteger.valueOf(1), r.getNumerator());
                assertEquals(BigInteger.valueOf(2), r.getDenominator());
            }
            else if((Long)e.getValue() == 2L) {
                List l = (List)e.getKey();
                assertEquals(1L, l.get(0));
                assertEquals(2L, l.get(1));
                assertEquals(3L, l.get(2));
            }
        }
    }

    public void testReadSetTagAsString() throws IOException {
        Object o = reader("{\"~~#set\": [1, 2, 3]}").read();
        assertFalse(o instanceof Set);
        assertTrue(o instanceof Map);
    }

    public void testReadMany() throws IOException {

        Reader r = reader("true null false \"foo\" 42.2 42");
        assertTrue((Boolean)r.read());
        assertNull(r.read());
        assertFalse((Boolean) r.read());
        assertEquals("foo", (String) r.read());
        assertEquals(42.2, (double) r.read());
        assertEquals(42L, (long) r.read());
    }

    public void testReadCache() {

        ReadCache rc = new ReadCache();
        assertEquals("~:foo", rc.cacheRead("~:foo", false));
        assertEquals("~:foo", rc.cacheRead("^" + (char) WriteCache.BASE_CHAR_IDX, false));
        assertEquals("~$bar", rc.cacheRead("~$bar", false));
        assertEquals("~$bar", rc.cacheRead("^" + (char)(WriteCache.BASE_CHAR_IDX + 1), false));
        assertEquals("~#baz", rc.cacheRead("~#baz", false));
        assertEquals("~#baz", rc.cacheRead("^" + (char) (WriteCache.BASE_CHAR_IDX + 2), false));
        assertEquals("foobar", rc.cacheRead("foobar", false));
        assertEquals("foobar", rc.cacheRead("foobar", false));
        assertEquals("foobar", rc.cacheRead("foobar", true));
        assertEquals("foobar", rc.cacheRead("^" + (char) (WriteCache.BASE_CHAR_IDX + 3), true));
        assertEquals("abc", rc.cacheRead("abc", false));
        assertEquals("abc", rc.cacheRead("abc", false));
        assertEquals("abc", rc.cacheRead("abc", true));
        assertEquals("abc", rc.cacheRead("abc", true));
    }

    public void testReadHandlerMapWithNoCustomHandlers() {
        assertEquals("foo", reader("\"foo\"", TransitFactory.readHandlerMap(null)).read());
    }

    public class Point {
        public final int x;
        public final int y;
        public Point(int x,int y) {this.x = x; this.y = y;}
        public String toString() { return "Point at " + x + ", " + y; }
        public boolean equals(Object other) { return other instanceof Point &&
                ((Point)other).x == x &&
                ((Point)other).y == y; }
        public int hashCode() { return x * y; }
    }

    public void testCustomReadHandler() throws Exception {
        Map<String, ReadHandler<?, ?>> customHandlers = new HashMap<String, ReadHandler<?, ?>>() {{
            put("point", new ReadHandler() {
                @Override
                public Object fromRep(Object o) {
                    List coords = (List) o;
                    int x = ((Long) coords.get(0)).intValue();
                    int y = ((Long) coords.get(1)).intValue();
                    return new Point(x,y);
                }
            });
        }};
        assertEquals(new Point(37,42), reader("[\"~#point\",[37,42]]", customHandlers).read());
    }

    public void testCustomDefaultReadHandler() {
        DefaultReadHandler readHandler = new DefaultReadHandler() {
            @Override
            public Object fromRep(String tag, Object rep) {
                return tag + ": " + rep.toString();
            }
        };

        Reader r = reader("[\"~#unknown\",[37,42]]", readHandler);
        assertEquals("unknown: [37, 42]", r.read());
    }

    // Writing

    public String write(Object o, TransitFactory.Format format) {
        OutputStream out = new ByteArrayOutputStream();
        Writer w = TransitFactory.writer(format, out);
        return write(o, w, out);
    }

    public String write(Object o, TransitFactory.Format format, Map<Class, WriteHandler<?, ?>> customHandlers) {
        OutputStream out = new ByteArrayOutputStream();
        Writer w = TransitFactory.writer(format, out, customHandlers);
        return write(o, w, out);
    }

    public String write(Object o, TransitFactory.Format format, WriteHandler<?, ?> customDefaultHandler) {
        OutputStream out = new ByteArrayOutputStream();
        Writer w = TransitFactory.writer(format, out, customDefaultHandler);
        return write(o, w, out);
    }

    public String write(Object o, Writer w, OutputStream out) {
        try {
            w.write(o);
        } catch (Throwable e) { throw new RuntimeException(e); }
        return out.toString();
    }


    public String writeJsonVerbose(Object o) {
        try {
            return write(o, TransitFactory.Format.JSON_VERBOSE);
        } catch (Throwable e) { throw new RuntimeException(e); }
    }

    public String writeJson(Object o) {
        try {
            return write(o, TransitFactory.Format.JSON);
        } catch (Throwable e) { throw new RuntimeException(e); }
    }

    public String scalar(String value) {
        return "[\"~#'\","+value+"]";
    }

    public String scalarVerbose(String value) {
        return "{\"~#'\":"+value+"}";
    }

    public void testWriteNull() throws Exception {

        assertEquals(scalarVerbose("null"), writeJsonVerbose(null));
        assertEquals(scalar("null"), writeJson(null));
    }

    public void testWriteKeyword() throws Exception {

        assertEquals(scalarVerbose("\"~:foo\""), writeJsonVerbose(TransitFactory.keyword("foo")));
        assertEquals(scalar("\"~:foo\""), writeJson(TransitFactory.keyword("foo")));

        List l = new ArrayList();
        l.add(TransitFactory.keyword("foo"));
        l.add(TransitFactory.keyword("foo"));
        l.add(TransitFactory.keyword("foo"));
        assertEquals("[\"~:foo\",\"~:foo\",\"~:foo\"]", writeJsonVerbose(l));
        assertEquals("[\"~:foo\",\"^0\",\"^0\"]", writeJson(l));
    }

    public void testWriteString() throws Exception {

        assertEquals(scalarVerbose("\"foo\""), writeJsonVerbose("foo"));
        assertEquals(scalar("\"foo\""), writeJson("foo"));
        assertEquals(scalarVerbose("\"~~foo\""), writeJsonVerbose("~foo"));
        assertEquals(scalar("\"~~foo\""), writeJson("~foo"));
    }

    public void testWriteBoolean() throws Exception {

        assertEquals(scalarVerbose("true"), writeJsonVerbose(true));
        assertEquals(scalar("true"), writeJson(true));
        assertEquals(scalar("false"), writeJson(false));

        Map m = new HashMap();
        m.put(true, 1);
        assertEquals("{\"~?t\":1}", writeJsonVerbose(m));
        assertEquals("[\"^ \",\"~?t\",1]", writeJson(m));
        Map m2 = new HashMap();
        m2.put(false, 1);
        assertEquals("{\"~?f\":1}", writeJsonVerbose(m2));
        assertEquals("[\"^ \",\"~?f\",1]", writeJson(m2));
    }

    public void testWriteInteger() throws Exception {

        assertEquals(scalarVerbose("42"), writeJsonVerbose(42));
        assertEquals(scalarVerbose("42"), writeJsonVerbose(42L));
        assertEquals(scalarVerbose("42"), writeJsonVerbose(new Byte("42")));
        assertEquals(scalarVerbose("42"), writeJsonVerbose(new Short("42")));
        assertEquals(scalarVerbose("42"), writeJsonVerbose(new Integer("42")));
        assertEquals(scalarVerbose("42"), writeJsonVerbose(new Long("42")));
        assertEquals(scalarVerbose("\"~n42\""), writeJsonVerbose(new BigInteger("42")));
        assertEquals(scalarVerbose("\"~n4256768765123454321897654321234567\""), writeJsonVerbose(new BigInteger("4256768765123454321897654321234567")));
    }

    public void testWriteIntegerAtJSONBoundaries() throws Exception {

        assertEquals(scalarVerbose("9007199254740991"),       writeJsonVerbose((long) Math.pow(2, 53) - 1));
        assertEquals(scalarVerbose("\"~i9007199254740992\""), writeJsonVerbose((long) Math.pow(2, 53)));

        assertEquals(scalarVerbose("-9007199254740991"),       writeJsonVerbose(1 - (long) Math.pow(2, 53)));
        assertEquals(scalarVerbose("\"~i-9007199254740992\""), writeJsonVerbose(0 - (long) Math.pow(2, 53)));
    }

    public void testWriteFloatDouble() throws Exception {

        assertEquals(scalarVerbose("42.5"), writeJsonVerbose(42.5));
        assertEquals(scalarVerbose("42.5"), writeJsonVerbose(new Float("42.5")));
        assertEquals(scalarVerbose("42.5"), writeJsonVerbose(new Double("42.5")));
    }

    public void testSpecialNumbers() throws Exception {
        assertEquals(scalar("\"~zNaN\""), writeJson(Double.NaN));
        assertEquals(scalar("\"~zINF\""), writeJson(Double.POSITIVE_INFINITY));
        assertEquals(scalar("\"~z-INF\""), writeJson(Double.NEGATIVE_INFINITY));

        assertEquals(scalar("\"~zNaN\""), writeJson(Float.NaN));
        assertEquals(scalar("\"~zINF\""), writeJson(Float.POSITIVE_INFINITY));
        assertEquals(scalar("\"~z-INF\""), writeJson(Float.NEGATIVE_INFINITY));

        assertEquals(scalarVerbose("\"~zNaN\""), writeJsonVerbose(Double.NaN));
        assertEquals(scalarVerbose("\"~zINF\""), writeJsonVerbose(Double.POSITIVE_INFINITY));
        assertEquals(scalarVerbose("\"~z-INF\""), writeJsonVerbose(Double.NEGATIVE_INFINITY));

        assertEquals(scalarVerbose("\"~zNaN\""), writeJsonVerbose(Float.NaN));
        assertEquals(scalarVerbose("\"~zINF\""), writeJsonVerbose(Float.POSITIVE_INFINITY));
        assertEquals(scalarVerbose("\"~z-INF\""), writeJsonVerbose(Float.NEGATIVE_INFINITY));
    }

    public void testWriteBigDecimal() throws Exception {

        assertEquals(scalarVerbose("\"~f42.5\""), writeJsonVerbose(new BigDecimal("42.5")));
    }

    public void testWriteTime() throws Exception {

        Date d = new Date();
        String dateString = AbstractParser.getDateTimeFormat().format(d);
        long dateLong = d.getTime();
        assertEquals(scalarVerbose("\"~t" + dateString + "\""), writeJsonVerbose(d));
        assertEquals(scalar("\"~m" + dateLong + "\""), writeJson(d));
    }

    public void testWriteUUID() throws Exception {

        UUID uuid = UUID.randomUUID();

        assertEquals(scalarVerbose("\"~u" + uuid.toString() + "\""), writeJsonVerbose(uuid));
    }

    public void testWriteURI() throws Exception {

        URI uri = TransitFactory.uri("http://www.foo.com");

        assertEquals(scalarVerbose("\"~rhttp://www.foo.com\""), writeJsonVerbose(uri));
    }

    public void testWriteBinary() throws Exception {

        byte[] bytes = "foobarbaz".getBytes();
        byte[] encodedBytes = Base64.encodeBase64(bytes);

        assertEquals(scalarVerbose("\"~b" + new String(encodedBytes) + "\""), writeJsonVerbose(bytes));
    }

    public void testWriteSymbol() throws Exception {

        assertEquals(scalarVerbose("\"~$foo\""), writeJsonVerbose(TransitFactory.symbol("foo")));
    }

    public void testWriteArray() throws Exception {

        List l = new ArrayList();
        l.add(1);
        l.add(2);
        l.add(3);

        assertEquals("[1,2,3]", writeJsonVerbose(l));
        assertEquals("[1,2,3]", writeJson(l));
    }

    public void testWritePrimitiveArrays() throws Exception {

        int[] ints = {1,2};
        assertEquals("[1,2]", writeJsonVerbose(ints));
        long[] longs = {1L,2L};
        assertEquals("[1,2]", writeJsonVerbose(longs));
        float[] floats = {1.5f,2.78f};
        assertEquals("[1.5,2.78]", writeJsonVerbose(floats));
        boolean[] bools = {true,false};
        assertEquals("[true,false]", writeJsonVerbose(bools));
        double[] doubles = {1.654d,2.8765d};
        assertEquals("[1.654,2.8765]", writeJsonVerbose(doubles));
        short[] shorts = {1,2};
        assertEquals("[1,2]", writeJsonVerbose(shorts));
        char[] chars = {53,47};
        assertEquals("[\"~c5\",\"~c/\"]", writeJsonVerbose(chars));
    }

    public void testWriteMap() throws Exception {

        Map m = new LinkedHashMap();
        m.put("foo", 1);
        m.put("bar", 2);

        assertEquals("{\"foo\":1,\"bar\":2}", writeJsonVerbose(m));
        assertEquals("[\"^ \",\"foo\",1,\"bar\",2]", writeJson(m));
    }

    public void testWriteEmptyMap() throws Exception {
        Map m = new HashMap();
        assertEquals("{}", writeJsonVerbose(m));
        assertEquals("[\"^ \"]", writeJson(m));
    }

    public void testWriteSet() throws Exception {

        Set s = new LinkedHashSet();
        s.add("foo");
        s.add("bar");

        assertEquals("{\"~#set\":[\"foo\",\"bar\"]}", writeJsonVerbose(s));
        assertEquals("[\"~#set\",[\"foo\",\"bar\"]]", writeJson(s));
    }

    public void testWriteEmptySet() throws Exception {

        Set s = new HashSet();
        assertEquals("{\"~#set\":[]}", writeJsonVerbose(s));
        assertEquals("[\"~#set\",[]]", writeJson(s));
    }

    public void testWriteList() throws Exception {

        List l = new LinkedList();
        l.add("foo");
        l.add("bar");

        assertEquals("{\"~#list\":[\"foo\",\"bar\"]}", writeJsonVerbose(l));
        assertEquals("[\"~#list\",[\"foo\",\"bar\"]]", writeJson(l));
    }

    public void testWriteEmptyList() throws Exception {

        List l = new LinkedList();
        assertEquals("{\"~#list\":[]}", writeJsonVerbose(l));
        assertEquals("[\"~#list\",[]]", writeJson(l));
    }

    public void testWriteCharacter() throws Exception {

        assertEquals(scalarVerbose("\"~cf\""), writeJsonVerbose('f'));
    }

    public void testWriteRatio() throws Exception {

        Ratio r = new RatioImpl(BigInteger.valueOf(1), BigInteger.valueOf(2));

        assertEquals("{\"~#ratio\":[\"~n1\",\"~n2\"]}", writeJsonVerbose(r));
        assertEquals("[\"~#ratio\",[\"~n1\",\"~n2\"]]", writeJson(r));
    }

    public void testWriteCmap() throws Exception {

        Ratio r = new RatioImpl(BigInteger.valueOf(1), BigInteger.valueOf(2));
        Map m = new HashMap();
        m.put(r, 1);
        assertEquals("{\"~#cmap\":[{\"~#ratio\":[\"~n1\",\"~n2\"]},1]}", writeJsonVerbose(m));
        assertEquals("[\"~#cmap\",[[\"~#ratio\",[\"~n1\",\"~n2\"]],1]]", writeJson(m));
    }

    public void testWriteCache() {

        WriteCache wc = new WriteCache(true);
        assertEquals("~:a", wc.cacheWrite("~:a", false));
        assertEquals("~:a", wc.cacheWrite("~:a", false));
        assertEquals("~:ab", wc.cacheWrite("~:ab", false));
        assertEquals("^" + (char)WriteCache.BASE_CHAR_IDX, wc.cacheWrite("~:ab", false));
        assertEquals("~$a", wc.cacheWrite("~$a", false));
        assertEquals("~$a", wc.cacheWrite("~$a", false));
        assertEquals("~$ab", wc.cacheWrite("~$ab", false));
        assertEquals("^" + (char)(WriteCache.BASE_CHAR_IDX + 1), wc.cacheWrite("~$ab", false));
        assertEquals("~#a", wc.cacheWrite("~#a", false));
        assertEquals("~#a", wc.cacheWrite("~#a", false));
        assertEquals("~#ab", wc.cacheWrite("~#ab", false));
        assertEquals("^" + (char)(WriteCache.BASE_CHAR_IDX + 2), wc.cacheWrite("~#ab", false));
        assertEquals("abc", wc.cacheWrite("abc", false));
        assertEquals("abc", wc.cacheWrite("abc", false));
        assertEquals("abc", wc.cacheWrite("abc", true));
        assertEquals("abc", wc.cacheWrite("abc", true));
        assertEquals("abcd", wc.cacheWrite("abcd", false));
        assertEquals("abcd", wc.cacheWrite("abcd", false));
        assertEquals("abcd", wc.cacheWrite("abcd", true));
        assertEquals("^" + (char)(WriteCache.BASE_CHAR_IDX + 3), wc.cacheWrite("abcd", true));
    }

    public void testWriteCacheDisabled() {

        WriteCache wc = new WriteCache(false);
        assertEquals("foobar", wc.cacheWrite("foobar", false));
        assertEquals("foobar", wc.cacheWrite("foobar", false));
        assertEquals("foobar", wc.cacheWrite("foobar", true));
        assertEquals("foobar", wc.cacheWrite("foobar", true));
    }

    public void testWriteUnknown() throws Exception {

        List l = new ArrayList();
        l.add("`jfoo");
        assertEquals("[\"~`jfoo\"]", writeJsonVerbose(l));
        assertEquals(scalarVerbose("\"~`jfoo\""), writeJsonVerbose("`jfoo"));
        List l2 = new ArrayList();
        l2.add(1L);
        l2.add(2L);
        assertEquals("{\"~#point\":[1,2]}", writeJsonVerbose(TransitFactory.taggedValue("point", l2)));
    }

    public void testUseKeywordAsMapKey() {

        Map m = new HashMap();
        m.put(TransitFactory.keyword("foo"), 1);
        m.put("foo", 2);
        m.put(TransitFactory.keyword("bar"), 3);
        m.put("bar", 4);

        assertEquals(1, m.get(TransitFactory.keyword("!foo".substring(1))));
        assertEquals(2, m.get("!foo".substring(1)));
        assertEquals(3, m.get(TransitFactory.keyword("!bar".substring(1))));
        assertEquals(4, m.get("!bar".substring(1)));
    }

    public void testUseSymbolAsMapKey() {

        Map m = new HashMap();
        m.put(TransitFactory.symbol("foo"), 1);
        m.put("foo", 2);
        m.put(TransitFactory.symbol("bar"), 3);
        m.put("bar", 4);

        assertEquals(1, m.get(TransitFactory.symbol("!foo".substring(1))));
        assertEquals(2, m.get("!foo".substring(1)));
        assertEquals(3, m.get(TransitFactory.symbol("!bar".substring(1))));
        assertEquals(4, m.get("!bar".substring(1)));
    }

    public void testKeywordEquality() {

        String s = "foo";

        Keyword k1 = TransitFactory.keyword("foo");
        Keyword k2 = TransitFactory.keyword("!foo".substring(1));
        Keyword k3 = TransitFactory.keyword("bar");

        assertEquals(k1, k2);
        assertEquals(k2, k1);
        assertFalse(k1.equals(k3));
        assertFalse(k3.equals(k1));
        assertFalse(s.equals(k1));
        assertFalse(k1.equals(s));
    }

    public void testKeywordHashCode() {

        String s = "foo";
        Keyword k1 = TransitFactory.keyword("foo");
        Keyword k2 = TransitFactory.keyword("!foo".substring(1));
        Keyword k3 = TransitFactory.keyword("bar");
        Symbol symbol = TransitFactory.symbol("bar");

        assertEquals(k1.hashCode(), k2.hashCode());
        assertFalse(k3.hashCode() == k1.hashCode());
        assertFalse(symbol.hashCode() == k1.hashCode());
        assertFalse(s.hashCode() == k1.hashCode());
    }

    public void testKeywordComparator() {

        List<Keyword> l = new ArrayList<Keyword>();
        l.add(TransitFactory.keyword("bbb"));
        l.add(TransitFactory.keyword("ccc"));
        l.add(TransitFactory.keyword("abc"));
        l.add(TransitFactory.keyword("dab"));

        Collections.sort(l);

        assertEquals(":abc", l.get(0).toString());
        assertEquals(":bbb", l.get(1).toString());
        assertEquals(":ccc", l.get(2).toString());
        assertEquals(":dab", l.get(3).toString());
    }

    public void testSymbolEquality() {

        String s = "foo";

        Symbol sym1 = TransitFactory.symbol("foo");
        Symbol sym2 = TransitFactory.symbol("!foo".substring(1));
        Symbol sym3 = TransitFactory.symbol("bar");

        assertEquals(sym1, sym2);
        assertEquals(sym2, sym1);
        assertFalse(sym1.equals(sym3));
        assertFalse(sym3.equals(sym1));
        assertFalse(s.equals(sym1));
        assertFalse(sym1.equals(s));
    }

    public void testSymbolHashCode() {

        String s = "foo";
        Symbol sym1 = TransitFactory.symbol("foo");
        Symbol sym2 = TransitFactory.symbol("!foo".substring(1));
        Symbol sym3 = TransitFactory.symbol("bar");
        Keyword symbol = TransitFactory.keyword("bar");

        assertEquals(sym1.hashCode(), sym2.hashCode());
        assertFalse(sym3.hashCode() == sym1.hashCode());
        assertFalse(symbol.hashCode() == sym1.hashCode());
        assertFalse(s.hashCode() == sym1.hashCode());
    }

    public void testSymbolComparator() {

        List<Symbol> l = new ArrayList<Symbol>();
        l.add(TransitFactory.symbol("bbb"));
        l.add(TransitFactory.symbol("ccc"));
        l.add(TransitFactory.symbol("abc"));
        l.add(TransitFactory.symbol("dab"));

        Collections.sort(l);

        assertEquals("abc", l.get(0).toString());
        assertEquals("bbb", l.get(1).toString());
        assertEquals("ccc", l.get(2).toString());
        assertEquals("dab", l.get(3).toString());
    }

    public void testMapWithEscapedKey() {
        Map m1 = new HashMap();
        m1.put("~Gfoo", 20L);
        String str = writeJson(m1);
        Map m2 = (Map) reader(str).read();
        assertTrue(m2.keySet().contains("~Gfoo"));
        assertTrue(m2.get("~Gfoo").equals(20L));
    }

    public void testLink() {
        Link l1 = TransitFactory.link("http://google.com", "search", "name", "link", "prompt");
        String str = writeJson(l1);
        Link l2 = (Link) reader(str).read();
        assertEquals("http://google.com", l2.getHref().getValue());
        assertEquals("search", l2.getRel());
        assertEquals("name", l2.getName());
        assertEquals("link", l2.getRender());
        assertEquals("prompt", l2.getPrompt());
    }

    public void testEmptySet() {
        String str = writeJson(new HashSet());
        assertTrue(reader(str).read() instanceof Set);
    }

    public void test() {
        Writer<Object> w = TransitFactory.writer(TransitFactory.Format.JSON, null);
        Writer<Map<String, String>> w2 = TransitFactory.writer(TransitFactory.Format.JSON, null);
        Writer w3 = TransitFactory.writer(TransitFactory.Format.JSON, null);
    }

    public void testPrettyPrint() {
        try {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            JsonFactory jf = new JsonFactory();
            JsonGenerator jg = jf.createGenerator(bytes);
            jg.writeString(":db/ident");
            jg.close();
            String s = new String(bytes.toByteArray());
            System.out.println(s);
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    public static class TestListWriteHandler extends AbstractWriteHandler<List<Object>, Object> {

        @Override
        public String tag(List<Object> o) {
            if (o instanceof RandomAccess) // ArrayList, Stack, Vector
                return "array";
            else
                return "list";
        }

        @Override
        public Object rep(List<Object> o) {
            if (o instanceof LinkedList)
                return TransitFactory.taggedValue("array", o);
            else
                return o;
        }
    }

    public void testWriteHandlerCache() {
        Map<Class, WriteHandler<?, ?>> handlers = new HashMap<Class, WriteHandler<?, ?>>();
        handlers.put(java.util.List.class, new TestListWriteHandler());

        for (int i = 0; i < 2; i++) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            Writer<Object> w = TransitFactory.writer(TransitFactory.Format.JSON, out, handlers);
        }
    }

    public void testWriteUnknownType() {
        try {
            write(new Point(1,2), TransitFactory.Format.JSON);
            throw new RuntimeException("Should not have gotten here.");
        } catch (Exception e) {
            assertTrue(String.format("expected %s, got %s", "Not supported", e.getMessage()),
                    e.getMessage().contains("Not supported"));
        }
    }

    public void testCustomDefaultWriteHandler() throws Exception {
        assertEquals("[\"~#unknown\",\"Point at 37, 42\"]", write(new Point(37,42),
                TransitFactory.Format.JSON,
                new WriteHandler() {
                    @Override
                    public String tag(Object o) { return "unknown"; }

                    @Override
                    public Object rep(Object o) { return o.toString(); }

                    @Override
                    public String stringRep(Object o) { return o.toString(); }

                    @Override
                    public WriteHandler getVerboseHandler() { return this; }
                }));
    }

    public void testCustomWriteHandler() {
        Map<Class, WriteHandler<?,?>> customHandlers = new HashMap<Class, WriteHandler<?,?>>(){{
            put(Point.class, new WriteHandler() {
                @Override
                public String tag(Object o) { return "point"; }

                @Override
                public Object rep(Object o) { return Arrays.asList(((Point)o).x, ((Point)o).y); }

                @Override
                public String stringRep(Object o) { return rep(o).toString(); }

                @Override
                public WriteHandler getVerboseHandler() { return this; }
            });
        }};
        assertEquals("[\"~#point\",[37,42]]",
                write(new Point(37, 42), TransitFactory.Format.JSON, TransitFactory.writeHandlerMap(customHandlers)));

    }

}