/*
 * Copyright (C) 2007, 2008, 2009, 2010, 2011, 2013, 2016, 2017, 2018, 2021 XStream Committers.
 * All rights reserved.
 *
 * The software in this package is published under the terms of the BSD
 * style license a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 * 
 * Created on 30. April 2007 by Joerg Schaible
 */
package com.thoughtworks.xstream.io.json;

import com.thoughtworks.acceptance.objects.Category;
import com.thoughtworks.acceptance.objects.OwnerOfExternalizable;
import com.thoughtworks.acceptance.objects.Product;
import com.thoughtworks.acceptance.objects.SomethingExternalizable;
import com.thoughtworks.acceptance.objects.StandardObject;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.JVM;
import com.thoughtworks.xstream.testutil.TimeZoneChanger;

import junit.framework.TestCase;

import org.codehaus.jettison.mapped.Configuration;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;


/**
 * Testing serialization to and from JSON with Jettison driver.
 * 
 * @author Dejan Bosanac
 */
public class JettisonMappedXmlDriverTest extends TestCase {

    private final static String SIMPLE = "{'product':{'name':'Banana','id':123,'price':23}}"
        .replace('\'', '"');
    private final static String HIERARCHY = (JVM.is15()
        ? "{'category':{'name':'fruit','id':111,'products':[{'product':[{'name':'Banana','id':123,'price':23.01,'tags':[{'string':['yellow','fresh','tasty']}]},{'name':'Mango','id':124,'price':34.01}]}]}}"
        : "{'category':{'name':'fruit','id':111,'products':{'product':[{'name':'Banana','id':123,'price':23.01,'tags':{'string':['yellow','fresh','tasty']}},{'name':'Mango','id':124,'price':34.01}]}}}")
        .replace('\'', '"');

    private XStream xstream;

    /**
     * @see junit.framework.TestCase#setUp()
     */
    protected void setUp() throws Exception {
        super.setUp();
        TimeZoneChanger.change("UTC");
        xstream = new XStream(new JettisonMappedXmlDriver());
        xstream.allowTypesByWildcard(new String[]{
            getClass().getName() + "$*", "com.thoughtworks.acceptance.objects.*"});
        xstream.alias("category", Category.class);
        xstream.alias("product", Product.class);
    }

    protected void tearDown() throws Exception {
        TimeZoneChanger.reset();
        super.tearDown();
    }

    public void testReadSimple() {
        Product product = (Product)xstream.fromXML(SIMPLE);
        assertEquals(product.getName(), "Banana");
        assertEquals(product.getId(), "123");
        assertEquals("" + product.getPrice(), "" + 23.00);
    }

    public void testWriteSimple() {
        Product product = new Product("Banana", "123", 23.00);
        String result = xstream.toXML(product);
        assertEquals(SIMPLE, result);
    }

    public void testJettisonConfigured()
        throws ClassNotFoundException, InstantiationException, IllegalAccessException,
        NoSuchMethodException, InvocationTargetException {
        if (JVM.is15()) {
            Object typeConverter = Class.class.forName(
                "org.codehaus.jettison.mapped.SimpleConverter").newInstance();
            Method setTypeConverter = Configuration.class.getMethod(
                "setTypeConverter", new Class[]{typeConverter.getClass().getInterfaces()[0]});
            Configuration config = new Configuration();
            setTypeConverter.invoke(config, new Object[]{typeConverter});
            xstream = new XStream(new JettisonMappedXmlDriver(config));
            xstream.allowTypesByWildcard(new String[]{"com.thoughtworks.acceptance.objects.*"});
            xstream.alias("product", Product.class);
            Product product = new Product("Banana", "123", 23.00);
            String result = xstream.toXML(product);
            assertEquals(
                "{'product':{'name':'Banana','id':'123','price':'23.0'}}".replace('\'', '"'),
                result);
            assertEquals(product, xstream.fromXML(result));
        }
    }

    public void testWriteHierarchy() {
        Category category = new Category("fruit", "111");
        ArrayList products = new ArrayList();
        Product banana = new Product("Banana", "123", 23.01);
        ArrayList bananaTags = new ArrayList();
        bananaTags.add("yellow");
        bananaTags.add("fresh");
        bananaTags.add("tasty");
        banana.setTags(bananaTags);
        products.add(banana);
        Product mango = new Product("Mango", "124", 34.01);
        products.add(mango);
        category.setProducts(products);
        String result = xstream.toXML(category);
        assertEquals(HIERARCHY, result);
    }

    public void testHierarchyRead() {
        Category parsedCategory = (Category)xstream.fromXML(HIERARCHY);
        Product parsedBanana = (Product)parsedCategory.getProducts().get(0);
        assertEquals("Banana", parsedBanana.getName());
        assertEquals(3, parsedBanana.getTags().size());
        assertEquals("yellow", parsedBanana.getTags().get(0));
        assertEquals("tasty", parsedBanana.getTags().get(2));
    }

    public void testObjectStream() throws IOException, ClassNotFoundException {
        Product product = new Product("Banana", "123", 23.00);
        StringWriter writer = new StringWriter();
        ObjectOutputStream oos = xstream.createObjectOutputStream(writer, "oos");
        oos.writeObject(product);
        oos.close();
        String json = writer.toString();
        assertEquals("{\"oos\":" + SIMPLE + "}", json);
        ObjectInputStream ois = xstream.createObjectInputStream(new StringReader(json));
        Product parsedProduct = (Product)ois.readObject();
        assertEquals(product.toString(), parsedProduct.toString());
    }

    public void testDoesHandleQuotesAndEscapes() {
        String[] strings = new String[]{
            "last\"", "\"first", "\"between\"", "around \"\" it", "back\\slash",
            "forward/slash"};
        String expected = (""
            + (JVM.is15() ? "{#string-array#:[{#string#:[" : "{#string-array#:{#string#:[")
            + "#last\\\"#,"
            + "#\\\"first#,"
            + "#\\\"between\\\"#,"
            + "#around \\\"\\\" it#,"
            + "#back\\\\slash#,"
            + "#forward\\/slash#" + (JVM.is15() ? "]}]}" : "]}}")).replace('#', '"');
        assertEquals(expected, xstream.toXML(strings));
    }

    public void testDoesEscapeValuesAccordingRfc4627() {
        String expected = "{'string':'\\u0000\\u0001\\u001f \uffee'}".replace('\'', '"');
        assertEquals(expected, xstream.toXML("\u0000\u0001\u001f\u0020\uffee"));
    }

    public void testListWithOneSimpleObject() {
        ArrayList list1 = new ArrayList();
        list1.add("one");
        String json = xstream.toXML(list1);
        assertEquals((JVM.is15()
            ? "{'list':[{'string':'one'}]}"
            : "{'list':{'string':['one']}}").replace('\'', '"'), json);
        ArrayList list2 = (ArrayList)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(list2));
    }

    public void testListWithSimpleObjects() {
        ArrayList list1 = new ArrayList();
        list1.add("one");
        list1.add("two");
        list1.add("three");
        String json = xstream.toXML(list1);
        assertEquals((JVM.is15()
            ? "{'list':[{'string':['one','two','three']}]}"
            : "{'list':{'string':['one','two','three']}}").replace('\'', '"'), json);
        ArrayList list2 = (ArrayList)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(list2));
    }

    public void testListWithDifferentSimpleObjects() {
        final ArrayList list1 = new ArrayList();
        list1.add("one");
        list1.add(new Integer(2));
        list1.add(new Float(3.3f));
        final String json = xstream.toXML(list1);
        assertEquals((JVM.is15()
            ? "{'list':[{'string':'one','int':2,'float':3.3}]}"
            : "{'list':{'string':['one'],'int':2,'float':3.3}}").replace('\'', '"'), json);
        final ArrayList list2 = (ArrayList)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(list2));
    }

    public void testSingletonListWithComplexObject() {
        Product product = new Product("Banana", "123", 23.00);
        ArrayList list1 = new ArrayList();
        list1.add(product);
        String json = xstream.toXML(list1);
        assertEquals((JVM.is15()
            ? "{'list':[{'product':{'name':'Banana','id':123,'price':23}}]}"
            : "{'list':{'product':[{'name':'Banana','id':123,'price':23}]}}")
            .replace('\'', '"'), json);
        ArrayList list2 = (ArrayList)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(list2));
    }

    public void testListWithComplexNestedObjects() {
        ArrayList list1 = new ArrayList();
        list1.add(new Product("Banana", "123", 23.00));
        list1.add(new Product("Apple", "47", 11.00));
        list1.add(new Product("Orange", "100", 42.00));
        ArrayList tags = new ArrayList();
        ((Product)list1.get(1)).setTags(tags);
        tags.add(new Product("Braeburn", "47.1", 10.00));
        String json = xstream.toXML(list1);
        assertEquals(
            (JVM.is15()
                ? "{'list':[{'product':[{'name':'Banana','id':123,'price':23},{'name':'Apple','id':47,'price':11,'tags':[{'product':{'name':'Braeburn','id':47.1,'price':10}}]},{'name':'Orange','id':100,'price':42}]}]}"
                : "{'list':{'product':[{'name':'Banana','id':123,'price':23},{'name':'Apple','id':47,'price':11,'tags':{'product':[{'name':'Braeburn','id':47.1,'price':10}]}},{'name':'Orange','id':100,'price':42}]}}")
                .replace('\'', '"'), json);
        ArrayList list2 = (ArrayList)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(list2));
    }

    public void todoTestEmptyList() {
        ArrayList list1 = new ArrayList();
        String json = xstream.toXML(list1);
        assertEquals("{'list':[]}".replace('\'', '"'), json);
        ArrayList list2 = (ArrayList)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(list2));
    }

    public static class Topic extends StandardObject {
        long id;
        String description;
        Date createdOn;
    }

    public void testDefaultValue() {
        Topic topic1 = new Topic();
        topic1.id = 4711;
        topic1.description = "JSON";
        topic1.createdOn = new Timestamp(1000);
        xstream.alias("topic", Topic.class);
        String json = xstream.toXML(topic1);
        assertEquals(
            "{'topic':{'id':4711,'description':'JSON','createdOn':{'@class':'sql-timestamp','$':'1970-01-01 00:00:01'}}}"
                .replace('\'', '"'), json);
        Topic topic2 = (Topic)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(topic2));
    }

    public void testLongValueWithHighPrecision() {
        Topic topic1 = new Topic();
        topic1.id = Long.MAX_VALUE;
        topic1.description = "JSON";
        xstream.alias("topic", Topic.class);
        String json = xstream.toXML(topic1);
        assertEquals(
            "{'topic':{'id':9223372036854775807,'description':'JSON'}}"
                .replace('\'', '"'), json);
        Topic topic2 = (Topic)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(topic2));
    }

    public void testEmbeddedXml() {
        ArrayList list1 = new ArrayList();
        list1.add("<xml attribute=\"foo\"><![CDATA[&quot;\"\'<>]]></xml>");
        String json = xstream.toXML(list1);
        assertEquals(
            (JVM.is15()
                ? "{\"list\":[{\"string\":\"<xml attribute=\\\"foo\\\"><![CDATA[&quot;\\\"'<>]]><\\/xml>\"}]}"
                : "{\"list\":{\"string\":[\"<xml attribute=\\\"foo\\\"><![CDATA[&quot;\\\"'<>]]><\\/xml>\"]}}"),
            json);
        ArrayList list2 = (ArrayList)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(list2));
    }

    public void testArrayList() {
        if (JVM.is15()) {
            ArrayList list1 = new ArrayList();
            list1.add(new Integer(12));

            list1.add("string");
            list1.add(new Integer(13));
            String json = xstream.toXML(list1);

            ArrayList list2 = (ArrayList)xstream.fromXML(json);
            assertEquals(json, xstream.toXML(list2));
        }
    }
    
    private static class SpecialCharacters extends StandardObject {
        String _foo__$_;
    }

    public void testSpecialNames() {
        SpecialCharacters sc = new SpecialCharacters();
        sc._foo__$_ = "bar";
        String json = xstream.toXML(sc);
        assertEquals(
            "{'com.thoughtworks.xstream.io.json.JettisonMappedXmlDriverTest$SpecialCharacters':{'_foo__$_':'bar'}}"
                .replace('\'', '"'), json);
        SpecialCharacters sc2 = (SpecialCharacters)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(sc2));
    }

    public void todoTestCanMarshalEmbeddedExternalizable() {
        xstream.alias("owner", OwnerOfExternalizable.class);
        
        OwnerOfExternalizable in = new OwnerOfExternalizable();
        in.target = new SomethingExternalizable("Joe", "Walnes");
        String json = xstream.toXML(in);
        // already wrong, Jettison reorders elements ...
        assertEquals("{'owner':{'target':{'int':3,'string':['JoeWalnes','XStream'],'null':''}}}".replace('\'', '"'), json);
        OwnerOfExternalizable owner = (OwnerOfExternalizable)xstream.fromXML(json);
        assertEquals(json, xstream.toXML(owner));
        assertEquals(in.target, owner.target);
    }
}
