/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.camel.dataformat.xstream;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.model.dataformat.XStreamDataFormat;
import org.apache.camel.test.junit4.CamelTestSupport;
import org.junit.Test;

/**
 * Marshal tests with domain objects.
 */
public class XStreamConfigurationTest extends CamelTestSupport {

    private static volatile boolean constructorInjected;
    private static volatile boolean methodInjected;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        constructorInjected = false;
        methodInjected = false;
    }

    public void testXStreamInjection() {
        assertTrue(constructorInjected);
        assertTrue(methodInjected);
    }

    @Test
    public void testCustomMarshalDomainObject() throws Exception {
        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedMessageCount(2);

        PurchaseOrder order = new PurchaseOrder();
        order.setName("Tiger");
        order.setAmount(1);
        order.setPrice(99.95);
        
        String ordereString = "<?xml version='1.0' encoding='UTF-8'?>" + "<purchase-order name=\"Tiger\" price=\"99.95\" amount=\"1.0\"/>";
        mock.expectedBodiesReceived(new Object[] {ordereString, order});

        this.template.sendBody("direct:marshal", order);
        this.template.sendBody("direct:unmarshal", ordereString);

        mock.assertIsSatisfied();
    }

    @Test
    public void testCustomMarshalDomainObjectWithImplicit() throws Exception {
        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedMessageCount(2);

        PurchaseHistory history = new PurchaseHistory();
        List<Double> list = new ArrayList<Double>();
        list.add(11.5);
        list.add(97.5);
        history.setHistory(list);

        String ordereString = "<?xml version='1.0' encoding='UTF-8'?>" + "<org.apache.camel.dataformat.xstream.PurchaseHistory>"
                + "<double>11.5</double><double>97.5</double>" + "</org.apache.camel.dataformat.xstream.PurchaseHistory>";
        mock.expectedBodiesReceived(new Object[] {ordereString, history});

        this.template.sendBody("direct:marshal", history);
        this.template.sendBody("direct:unmarshal", ordereString);

        mock.assertIsSatisfied();
    }

    @Test
    public void testCustomMarshalDomainObjectJson() throws Exception {
        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedMessageCount(2);

        PurchaseOrder order = new PurchaseOrder();
        order.setName("Tiger");
        order.setAmount(1);
        order.setPrice(99.95);
        
        String ordereString = "{\"purchase-order\":{\"@name\":\"Tiger\",\"@price\":\"99.95\",\"@amount\":\"1.0\"}}";
        mock.expectedBodiesReceived(new Object[] {ordereString, order});

        this.template.sendBody("direct:marshal-json", order);
        this.template.sendBody("direct:unmarshal-json", ordereString);

        mock.assertIsSatisfied();
    }
    
    @Test
    public void testCustomXStreamDriverMarshal() throws Exception {
        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedMessageCount(1);

        PurchaseOrder order = new PurchaseOrder();
        order.setName("Tiger");
        order.setAmount(1);
        order.setPrice(99.95);
                
        this.template.sendBody("direct:myDriver", order);
        mock.assertIsSatisfied();
        String result = mock.getExchanges().get(0).getIn().getBody(String.class);
        // make sure the result is start with "{"
        assertTrue("Should get a json result", result.startsWith("{"));
    }

    @Override
    protected RouteBuilder createRouteBuilder() throws Exception {
        return new RouteBuilder() {
            @Override
            public void configure() throws Exception {
                XStreamDataFormat xstreamDefinition = new XStreamDataFormat();
                Map<String, String> aliases = new HashMap<String, String>();
                aliases.put("purchase-order", PurchaseOrder.class.getName());
                xstreamDefinition.setAliases(aliases);

                List<String> converters = new ArrayList<String>();
                converters.add(PurchaseOrderConverter.class.getName());
                converters.add(CheckMethodInjection.class.getName());
                converters.add(CheckConstructorInjection.class.getName());

                xstreamDefinition.setConverters(converters);

                Map<String, String[]> implicits = new HashMap<String, String[]>();
                implicits.put(PurchaseHistory.class.getName(), new String[] {"history"});
                xstreamDefinition.setImplicitCollections(implicits);

                from("direct:marshal").marshal(xstreamDefinition).to("mock:result");
                from("direct:unmarshal").unmarshal(xstreamDefinition).to("mock:result");

                xstreamDefinition = new XStreamDataFormat();
                xstreamDefinition.setDriver("json");
                aliases = new HashMap<String, String>();
                aliases.put("purchase-order", PurchaseOrder.class.getName());
                xstreamDefinition.setAliases(aliases);

                converters = new ArrayList<String>();
                converters.add(PurchaseOrderConverter.class.getName());
                xstreamDefinition.setConverters(converters);
                from("direct:marshal-json").marshal(xstreamDefinition).to("mock:result");
                from("direct:unmarshal-json").unmarshal(xstreamDefinition).to("mock:result");
                
                org.apache.camel.dataformat.xstream.XStreamDataFormat xStreamDataFormat 
                    = new org.apache.camel.dataformat.xstream.XStreamDataFormat();
                xStreamDataFormat.setXstreamDriver(new JsonHierarchicalStreamDriver());
                from("direct:myDriver").marshal(xStreamDataFormat).to("mock:result");
            }
        };
    }

    public static class PurchaseOrderConverter implements Converter {

        public boolean canConvert(Class type) {
            return PurchaseOrder.class.isAssignableFrom(type);
        }

        public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
            PurchaseOrder order = new PurchaseOrder();
            order.setName(reader.getAttribute("name"));
            order.setPrice(Double.parseDouble(reader.getAttribute("price")));
            order.setAmount(Double.parseDouble(reader.getAttribute("amount")));
            return order;
        }

        public void marshal(Object object, HierarchicalStreamWriter writer, MarshallingContext context) {

            writer.addAttribute("name", ((PurchaseOrder) object).getName());
            writer.addAttribute("price", Double.toString(((PurchaseOrder) object).getPrice()));
            writer.addAttribute("amount", Double.toString(((PurchaseOrder) object).getAmount()));
        }
    }

    public static class CheckConstructorInjection implements Converter {
        public CheckConstructorInjection(XStream xstream) {
            if (xstream != null) {
                constructorInjected = true;
            } else {
                throw new RuntimeException("XStream should not be null");
            }
        }

        public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
        }

        public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
            return null;
        }

        public boolean canConvert(Class type) {
            return false;
        }
    }

    public static class CheckMethodInjection implements Converter {
        public CheckMethodInjection() {

        }

        public void setXStream(XStream xstream) {
            if (xstream != null) {
                methodInjected = true;
            } else {
                throw new RuntimeException("XStream should not be null");
            }
        }

        public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
        }

        public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
            return null;
        }

        public boolean canConvert(Class type) {
            return false;
        }
    }
}
