/**
 * 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.impl;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Properties;

import org.apache.camel.BeanInject;
import org.apache.camel.CamelContext;
import org.apache.camel.Consume;
import org.apache.camel.ContextTestSupport;
import org.apache.camel.EndpointInject;
import org.apache.camel.Exchange;
import org.apache.camel.NoSuchBeanException;
import org.apache.camel.PollingConsumer;
import org.apache.camel.Produce;
import org.apache.camel.Producer;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.PropertyInject;
import org.apache.camel.ResolveEndpointFailedException;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.component.properties.PropertiesComponent;
import org.apache.camel.support.SynchronizationAdapter;
import org.apache.camel.util.ObjectHelper;

/**
 * @version 
 */
public class CamelPostProcessorHelperTest extends ContextTestSupport {

    private MySynchronization mySynchronization = new MySynchronization();
    private Properties myProp = new Properties();

    @Override
    protected JndiRegistry createRegistry() throws Exception {
        JndiRegistry jndi = super.createRegistry();
        jndi.bind("myProp", myProp);
        jndi.bind("foo", new FooBar());
        return jndi;
    }

    @Override
    protected CamelContext createCamelContext() throws Exception {
        CamelContext context = super.createCamelContext();

        PropertiesComponent pc = context.getComponent("properties", PropertiesComponent.class);
        pc.setLocation("ref:myProp");

        return context;
    }

    public void testConstructor() {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper();
        assertNull(helper.getCamelContext());

        helper.setCamelContext(context);
        assertNotNull(helper.getCamelContext());
    }

    public void testConstructorCamelContext() {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);
        assertNotNull(helper.getCamelContext());
    }

    public void testMatchContext() {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);
        assertTrue(helper.matchContext(context.getName()));
        assertFalse(helper.matchContext("foo"));
    }

    public void testConsume() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyConsumeBean my = new MyConsumeBean();
        Method method = my.getClass().getMethod("consumeSomething", String.class);
        helper.consumerInjection(method, my, "foo");

        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedBodiesReceived("Hello World");

        template.sendBody("seda:foo", "Hello World");

        assertMockEndpointsSatisfied();
    }

    public void testConsumePrivate() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyPrivateConsumeBean my = new MyPrivateConsumeBean();
        Method method = my.getClass().getDeclaredMethod("consumeSomethingPrivate", String.class);
        try {
            helper.consumerInjection(method, my, "foo");
            fail("Should have thrown exception");
        } catch (RuntimeCamelException e) {
            IllegalArgumentException iae = assertIsInstanceOf(IllegalArgumentException.class, e.getCause());
            assertTrue(iae.getMessage().startsWith("The method private void"));
            assertTrue(iae.getMessage().endsWith("(for example the method must be public)"));
        }
    }

    public void testConsumeSynchronization() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyConsumeAndSynchronizationBean my = new MyConsumeAndSynchronizationBean();
        Method method = my.getClass().getMethod("consumeSomething", String.class, Exchange.class);
        helper.consumerInjection(method, my, "foo");

        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedBodiesReceived("Hello World");

        template.sendBody("seda:foo", "Hello World");

        assertMockEndpointsSatisfied();

        // give UoW a bit of time
        Thread.sleep(500);

        assertTrue("Should have invoked onDone", mySynchronization.isOnDone());
    }

    public void testProduceSynchronization() throws Exception {
        MyProduceAndSynchronizationBean my = new MyProduceAndSynchronizationBean();

        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);
        Producer producer = helper.createInjectionProducer(context.getEndpoint("mock:result"), my, "foo");
        my.setProducer(producer);

        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedBodiesReceived("Hello World");

        my.produceSomething("Hello World");

        assertMockEndpointsSatisfied();

        // give UoW a bit of time
        Thread.sleep(500);

        assertTrue("Should have invoked onDone", mySynchronization.isOnDone());
    }

    public void testEndpointInjectProducerTemplate() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyEndpointInjectBeanProducerTemplate bean = new MyEndpointInjectBeanProducerTemplate();
        Method method = bean.getClass().getMethod("setProducer", ProducerTemplate.class);

        EndpointInject endpointInject = method.getAnnotation(EndpointInject.class);
        Class<?>[] parameterTypes = method.getParameterTypes();
        for (Class<?> type : parameterTypes) {
            String propertyName = ObjectHelper.getPropertyName(method);
            Object value = helper.getInjectionValue(type, endpointInject.uri(), endpointInject.ref(), endpointInject.property(), propertyName, bean, "foo");
            ObjectHelper.invokeMethod(method, bean, value);
        }

        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedBodiesReceived("Hello World");

        assertNotNull(bean.getProducer());
        bean.send("Hello World");

        assertMockEndpointsSatisfied();
    }

    public void testEndpointInjectProducer() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyEndpointBeanProducer bean = new MyEndpointBeanProducer();
        Method method = bean.getClass().getMethod("setProducer", Producer.class);

        EndpointInject endpointInject = method.getAnnotation(EndpointInject.class);
        Class<?>[] parameterTypes = method.getParameterTypes();
        for (Class<?> type : parameterTypes) {
            String propertyName = ObjectHelper.getPropertyName(method);
            Object value = helper.getInjectionValue(type, endpointInject.uri(), endpointInject.ref(), endpointInject.property(), propertyName, bean, "foo");
            ObjectHelper.invokeMethod(method, bean, value);
        }

        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedBodiesReceived("Hello World");

        assertNotNull(bean.getProducer());

        Exchange exchange = new DefaultExchange(context);
        exchange.getIn().setBody("Hello World");

        bean.send(exchange);

        assertMockEndpointsSatisfied();
    }

    public void testEndpointInjectPollingConsumer() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyEndpointBeanPollingConsumer bean = new MyEndpointBeanPollingConsumer();
        Method method = bean.getClass().getMethod("setConsumer", PollingConsumer.class);

        EndpointInject endpointInject = method.getAnnotation(EndpointInject.class);
        Class<?>[] parameterTypes = method.getParameterTypes();
        for (Class<?> type : parameterTypes) {
            String propertyName = ObjectHelper.getPropertyName(method);
            Object value = helper.getInjectionValue(type, endpointInject.uri(), endpointInject.ref(), endpointInject.property(), propertyName, bean, "foo");
            ObjectHelper.invokeMethod(method, bean, value);
        }

        template.sendBody("seda:foo", "Hello World");

        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedBodiesReceived("Hello World");

        assertNotNull(bean.getConsumer());

        Exchange exchange = bean.consume();
        template.send("mock:result", exchange);

        assertMockEndpointsSatisfied();
    }

    public void testEndpointInjectProducerTemplateField() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyEndpointInjectProducerTemplate bean = new MyEndpointInjectProducerTemplate();
        Field field = bean.getClass().getField("producer");

        EndpointInject endpointInject = field.getAnnotation(EndpointInject.class);
        Class<?> type = field.getType();
        String propertyName = "producer";
        Object value = helper.getInjectionValue(type, endpointInject.uri(), endpointInject.ref(), endpointInject.property(), propertyName, bean, "foo");

        field.set(bean, value);

        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedBodiesReceived("Hello World");

        Exchange exchange = new DefaultExchange(context);
        exchange.getIn().setBody("Hello World");

        bean.send(exchange);

        assertMockEndpointsSatisfied();
    }

    public void testEndpointInjectProducerTemplateFieldNoDefaultEndpoint() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyEndpointInjectProducerTemplateNoDefaultEndpoint bean = new MyEndpointInjectProducerTemplateNoDefaultEndpoint();
        Field field = bean.getClass().getField("producer");

        EndpointInject endpointInject = field.getAnnotation(EndpointInject.class);
        Class<?> type = field.getType();
        String propertyName = "producer";
        Object value = helper.getInjectionValue(type, endpointInject.uri(), endpointInject.ref(), endpointInject.property(), propertyName, bean, "foo");

        field.set(bean, value);

        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.expectedBodiesReceived("Hello World");

        Exchange exchange = new DefaultExchange(context);
        exchange.getIn().setBody("Hello World");

        bean.send(exchange);

        assertMockEndpointsSatisfied();
    }

    public void testEndpointInjectProducerTemplateFieldNameUnknown() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyEndpointInjectProducerTemplateNameUnknown bean = new MyEndpointInjectProducerTemplateNameUnknown();
        Field field = bean.getClass().getField("producer");

        EndpointInject endpointInject = field.getAnnotation(EndpointInject.class);
        Class<?> type = field.getType();
        String propertyName = "producer";

        try {
            helper.getInjectionValue(type, endpointInject.uri(), endpointInject.ref(), endpointInject.property(), propertyName, bean, "foo");
            fail("Should throw exception");
        } catch (NoSuchBeanException e) {
            assertEquals("No bean could be found in the registry for: unknown of type: org.apache.camel.Endpoint", e.getMessage());
        }
    }

    public void testEndpointInjectProducerTemplateFieldUrlUnknown() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyEndpointInjectProducerTemplateUrlUnknown bean = new MyEndpointInjectProducerTemplateUrlUnknown();
        Field field = bean.getClass().getField("producer");

        EndpointInject endpointInject = field.getAnnotation(EndpointInject.class);
        Class<?> type = field.getType();
        String propertyName = "producer";

        try {
            helper.getInjectionValue(type, endpointInject.uri(), endpointInject.ref(), endpointInject.property(), propertyName, bean, "foo");
            fail("Should throw exception");
        } catch (ResolveEndpointFailedException e) {
            assertEquals("Failed to resolve endpoint: xxx://foo due to: No component found with scheme: xxx", e.getMessage());
        }
    }

    public void testEndpointInjectBothUriAndRef() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyEndpointBothUriAndRef bean = new MyEndpointBothUriAndRef();
        Field field = bean.getClass().getField("producer");

        EndpointInject endpointInject = field.getAnnotation(EndpointInject.class);
        Class<?> type = field.getType();
        String propertyName = "producer";

        try {
            helper.getInjectionValue(type, endpointInject.uri(), endpointInject.ref(), endpointInject.property(), propertyName, bean, "foo");
            fail("Should throw exception");
        } catch (IllegalArgumentException e) {
            assertEquals("Both uri and name is provided, only either one is allowed: uri=seda:foo, ref=myEndpoint", e.getMessage());
        }
    }

    public void testPropertyFieldInject() throws Exception {
        myProp.put("myTimeout", "2000");
        myProp.put("myApp", "Camel");

        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyPropertyFieldBean bean = new MyPropertyFieldBean();

        Field field = bean.getClass().getField("timeout");
        PropertyInject propertyInject = field.getAnnotation(PropertyInject.class);
        Class<?> type = field.getType();
        Object value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", "timeout",  bean, "foo");
        assertEquals(Integer.valueOf("2000"), Integer.valueOf("" + value));

        field = bean.getClass().getField("greeting");
        propertyInject = field.getAnnotation(PropertyInject.class);
        type = field.getType();
        value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", "greeting",  bean, "foo");
        assertEquals("Hello Camel", value);
    }

    public void testPropertyFieldDefaultValueInject() throws Exception {
        myProp.put("myApp", "Camel");

        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyPropertyFieldBean bean = new MyPropertyFieldBean();

        Field field = bean.getClass().getField("timeout");
        PropertyInject propertyInject = field.getAnnotation(PropertyInject.class);
        Class<?> type = field.getType();
        Object value = helper.getInjectionPropertyValue(type, propertyInject.value(), "5000", "timeout",  bean, "foo");
        assertEquals(Integer.valueOf("5000"), Integer.valueOf("" + value));

        field = bean.getClass().getField("greeting");
        propertyInject = field.getAnnotation(PropertyInject.class);
        type = field.getType();
        value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", "greeting",  bean, "foo");
        assertEquals("Hello Camel", value);
    }

    public void testPropertyMethodInject() throws Exception {
        myProp.put("myTimeout", "2000");
        myProp.put("myApp", "Camel");

        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyPropertyMethodBean bean = new MyPropertyMethodBean();

        Method method = bean.getClass().getMethod("setTimeout", int.class);
        PropertyInject propertyInject = method.getAnnotation(PropertyInject.class);
        Class<?> type = method.getParameterTypes()[0];
        Object value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", "timeout",  bean, "foo");
        assertEquals(Integer.valueOf("2000"), Integer.valueOf("" + value));

        method = bean.getClass().getMethod("setGreeting", String.class);
        propertyInject = method.getAnnotation(PropertyInject.class);
        type = method.getParameterTypes()[0];
        value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", "greeting",  bean, "foo");
        assertEquals("Hello Camel", value);
    }

    public void testBeanInject() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyBeanInjectBean bean = new MyBeanInjectBean();
        Field field = bean.getClass().getField("foo");

        BeanInject beanInject = field.getAnnotation(BeanInject.class);
        Class<?> type = field.getType();
        Object value = helper.getInjectionBeanValue(type, beanInject.value());
        field.set(bean, value);

        String out = bean.doSomething("World");
        assertEquals("Hello World", out);
    }

    public void testBeanInjectNotFound() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyBeanInjectBean bean = new MyBeanInjectBean();
        Field field = bean.getClass().getField("foo");

        Class<?> type = field.getType();
        try {
            helper.getInjectionBeanValue(type, "bar");
            fail("Should have thrown exception");
        } catch (NoSuchBeanException e) {
            assertEquals("No bean could be found in the registry for: bar of type: org.apache.camel.impl.FooBar", e.getMessage());
            assertEquals("bar", e.getName());
        }
    }

    public void testBeanInjectByType() throws Exception {
        CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context);

        MyBeanInjectByTypeBean bean = new MyBeanInjectByTypeBean();
        Field field = bean.getClass().getField("foo");

        BeanInject beanInject = field.getAnnotation(BeanInject.class);
        Class<?> type = field.getType();
        Object value = helper.getInjectionBeanValue(type, beanInject.value());
        field.set(bean, value);

        String out = bean.doSomething("Camel");
        assertEquals("Hello Camel", out);
    }

    public class MyConsumeBean {

        @Consume(uri = "seda:foo")
        public void consumeSomething(String body) {
            assertEquals("Hello World", body);
            template.sendBody("mock:result", body);
        }
    }

    public class MyConsumeAndSynchronizationBean {

        @Consume(uri = "seda:foo")
        public void consumeSomething(String body, Exchange exchange) {
            exchange.addOnCompletion(mySynchronization);
            assertEquals("Hello World", body);
            template.sendBody("mock:result", body);
        }
    }

    public class MyProduceAndSynchronizationBean {

        @Produce(uri = "mock:result")
        Producer producer;

        public void produceSomething(String body) throws Exception {
            assertEquals("Hello World", body);

            Exchange exchange = producer.getEndpoint().createExchange();
            exchange.addOnCompletion(mySynchronization);
            exchange.getIn().setBody(body);
            producer.process(exchange);
        }

        public void setProducer(Producer producer) {
            this.producer = producer;
        }
    }

    private static class MySynchronization extends SynchronizationAdapter {

        private boolean onDone;

        @Override
        public void onDone(Exchange exchange) {
            onDone = true;
        }

        public boolean isOnDone() {
            return onDone;
        }
    }

    public class MyEndpointInjectBeanProducerTemplate {

        private ProducerTemplate producer;

        @EndpointInject(uri = "mock:result")
        public void setProducer(ProducerTemplate producer) {
            this.producer = producer;
        }

        public ProducerTemplate getProducer() {
            return producer;
        }

        public void send(String message) {
            producer.sendBody(message);
        }
    }

    public class MyEndpointBeanProducer {

        private Producer producer;

        @EndpointInject(uri = "mock:result")
        public void setProducer(Producer producer) {
            this.producer = producer;
        }

        public Producer getProducer() {
            return producer;
        }

        public void send(Exchange exchange) throws Exception {
            producer.process(exchange);
        }

    }

    public class MyEndpointBeanPollingConsumer {

        private PollingConsumer consumer;

        @EndpointInject(uri = "seda:foo")
        public void setConsumer(PollingConsumer consumer) {
            this.consumer = consumer;
        }

        public PollingConsumer getConsumer() {
            return consumer;
        }

        public Exchange consume() throws Exception {
            return consumer.receive(1000);
        }

    }

    public class MyEndpointInjectProducerTemplate {

        @EndpointInject(uri = "mock:result")
        public ProducerTemplate producer;

        public void send(Exchange exchange) throws Exception {
            producer.send(exchange);
        }

    }

    public class MyEndpointInjectProducerTemplateNoDefaultEndpoint {

        @EndpointInject()
        public ProducerTemplate producer;

        public void send(Exchange exchange) throws Exception {
            producer.send("mock:result", exchange);
        }

    }

    public class MyEndpointInjectProducerTemplateNameUnknown {

        @EndpointInject(ref = "unknown")
        public ProducerTemplate producer;

        public void send(Exchange exchange) throws Exception {
            producer.send(exchange);
        }

    }

    public class MyEndpointInjectProducerTemplateUrlUnknown {

        @EndpointInject(uri = "xxx:foo")
        public ProducerTemplate producer;

        public void send(Exchange exchange) throws Exception {
            producer.send(exchange);
        }

    }

    public class MyEndpointBothUriAndRef {

        @EndpointInject(uri = "seda:foo", ref = "myEndpoint")
        public ProducerTemplate producer;

        public void send(Exchange exchange) throws Exception {
            producer.send(exchange);
        }

    }

    public class MyPrivateConsumeBean {
        
        @Consume(uri = "seda:foo")
        private void consumeSomethingPrivate(String body) {
            assertEquals("Hello World", body);
            template.sendBody("mock:result", body);
        }
    }

    public class MyPropertyFieldBean {

        @PropertyInject("myTimeout")
        public int timeout;

        @PropertyInject("Hello {{myApp}}")
        public String greeting;

        public String doSomething(String body) {
            return greeting + " " + body + " with timeout=" + timeout;
        }
    }

    public class MyPropertyFieldDefaultValueBean {

        @PropertyInject(value = "myTimeout", defaultValue = "5000")
        public int timeout;

        @PropertyInject("Hello {{myApp}}")
        public String greeting;

        public String doSomething(String body) {
            return greeting + " " + body + " with timeout=" + timeout;
        }
    }

    public class MyPropertyMethodBean {

        private int timeout;
        private String greeting;

        public String doSomething(String body) {
            return greeting + " " + body + " with timeout=" + timeout;
        }

        public int getTimeout() {
            return timeout;
        }

        @PropertyInject("myTimeout")
        public void setTimeout(int timeout) {
            this.timeout = timeout;
        }

        public String getGreeting() {
            return greeting;
        }

        @PropertyInject("Hello {{myApp}}")
        public void setGreeting(String greeting) {
            this.greeting = greeting;
        }
    }

    public class MyBeanInjectBean {

        @BeanInject("foo")
        public FooBar foo;

        public String doSomething(String body) {
            return foo.hello(body);
        }
    }

    public class MyBeanInjectByTypeBean {

        @BeanInject
        public FooBar foo;

        public String doSomething(String body) {
            return foo.hello(body);
        }
    }



}
