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

import java.net.ConnectException;
import java.util.concurrent.atomic.AtomicInteger;

import javax.sql.DataSource;

import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.builder.NotifyBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.itest.ITestSupport;
import org.apache.camel.spi.IdempotentRepository;
import org.apache.camel.test.spring.CamelSpringTestSupport;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

/**
 * Jms with JDBC idempotent consumer test.
 */
public class FromJmsToJdbcIdempotentConsumerToJmsTest extends CamelSpringTestSupport {

    protected JdbcTemplate jdbcTemplate;
    protected DataSource dataSource;

    @Override
    protected AbstractApplicationContext createApplicationContext() {
        ITestSupport.getPort2();
        return new ClassPathXmlApplicationContext("org/apache/camel/itest/sql/FromJmsToJdbcIdempotentConsumerToJmsTest.xml");
    }

    @Override
    @Before
    public void setUp() throws Exception {
        super.setUp();

        dataSource = context.getRegistry().lookupByNameAndType(getDatasourceName(), DataSource.class);
        jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.afterPropertiesSet();

        // cater for slow servers
        getMockEndpoint("mock:a").setResultWaitTime(30000);
        getMockEndpoint("mock:b").setResultWaitTime(30000);
    }

    protected String getDatasourceName() {
        return "myNonXADataSource";
    }

    @Test
    public void testJmsToJdbcJmsCommit() throws Exception {
        checkInitialState();

        // use a notify to know when the message is done
        NotifyBuilder notify = new NotifyBuilder(context).whenDone(1).create();

        // use mock during testing as well
        getMockEndpoint("mock:a").expectedMessageCount(1);
        getMockEndpoint("mock:b").expectedMessageCount(1);

        template.sendBodyAndHeader("activemq2:queue:inbox", "A", "uid", 123);

        // assert mock and wait for the message to be done
        assertMockEndpointsSatisfied();
        assertTrue("Should complete 1 message", notify.matchesMockWaitTime());

        // check that there is a message in the database and JMS queue
        assertEquals(new Integer(1), jdbcTemplate.queryForObject("select count(*) from CAMEL_MESSAGEPROCESSED", Integer.class));
        Object out = consumer.receiveBody("activemq2:queue:outbox", 3000);
        assertEquals("DONE-A", out);
    }

    @Ignore("see the TODO below")
    @Test
    public void testJmsToJdbcJmsRollbackAtA() throws Exception {
        checkInitialState();

        // use a notify to know that after 1+6 (1 original + 6 redelivery) attempts from AcitveMQ
        NotifyBuilder notify = new NotifyBuilder(context).whenDone(7).create();

        // TODO: occasionally we get only 6 instead of 7 expected exchanges which's most probably an issue in ActiveMQ itself
        getMockEndpoint("mock:a").expectedMessageCount(7);
        // force exception to occur at mock a
        getMockEndpoint("mock:a").whenAnyExchangeReceived(new Processor() {
            @Override
            public void process(Exchange exchange) throws Exception {
                throw new ConnectException("Forced cannot connect to database");
            }
        });
        getMockEndpoint("mock:b").expectedMessageCount(0);

        template.sendBodyAndHeader("activemq2:queue:inbox", "A", "uid", 123);

        // assert mock and wait for the message to be done
        assertMockEndpointsSatisfied();
        assertTrue("Should complete 7 message", notify.matchesMockWaitTime());

        // check that there is a message in the database and JMS queue
        assertEquals(new Integer(0), jdbcTemplate.queryForObject("select count(*) from CAMEL_MESSAGEPROCESSED", Integer.class));
        assertNull(consumer.receiveBody("activemq2:queue:outbox", 3000));

        // the message should have been moved to the AMQ DLQ queue
        assertEquals("A", consumer.receiveBody("activemq2:queue:ActiveMQ.DLQ", 3000));
    }

    @Ignore("see the TODO below")
    @Test
    public void testJmsToJdbcJmsRollbackAtB() throws Exception {
        checkInitialState();

        // use a notify to know that after 1+6 (1 original + 6 redelivery) attempts from AcitveMQ
        NotifyBuilder notify = new NotifyBuilder(context).whenDone(7).create();

        // TODO: occasionally we get only 6 instead of 7 expected exchanges which's most probably an issue in ActiveMQ itself
        getMockEndpoint("mock:a").expectedMessageCount(7);
        getMockEndpoint("mock:b").expectedMessageCount(7);
        // force exception to occur at mock b
        getMockEndpoint("mock:b").whenAnyExchangeReceived(new Processor() {
            @Override
            public void process(Exchange exchange) throws Exception {
                throw new ConnectException("Forced cannot send to AMQ queue");
            }
        });

        template.sendBodyAndHeader("activemq2:queue:inbox", "B", "uid", 456);

        // assert mock and wait for the message to be done
        assertMockEndpointsSatisfied();
        assertTrue("Should complete 7 messages", notify.matchesMockWaitTime());

        // check that there is a message in the database and JMS queue
        assertEquals(new Integer(0), jdbcTemplate.queryForObject("select count(*) from CAMEL_MESSAGEPROCESSED", Integer.class));
        assertNull(consumer.receiveBody("activemq2:queue:outbox", 3000));

        // the message should have been moved to the AMQ DLQ queue
        assertEquals("B", consumer.receiveBody("activemq2:queue:ActiveMQ.DLQ", 3000));
    }

    @Test
    public void testFilterIdempotent() throws Exception {
        checkInitialState();

        // use a notify to know when the message is done
        NotifyBuilder notify = new NotifyBuilder(context).whenDone(3).create();

        // use mock during testing as well
        getMockEndpoint("mock:a").expectedMessageCount(3);
        // there should be 1 duplicate
        getMockEndpoint("mock:b").expectedMessageCount(2);

        template.sendBodyAndHeader("activemq2:queue:inbox", "D", "uid", 111);
        template.sendBodyAndHeader("activemq2:queue:inbox", "E", "uid", 222);
        template.sendBodyAndHeader("activemq2:queue:inbox", "D", "uid", 111);

        // assert mock and wait for the message to be done
        assertMockEndpointsSatisfied();
        assertTrue("Should complete 3 messages", notify.matchesMockWaitTime());

        // check that there is two messages in the database and JMS queue
        assertEquals(new Integer(2), jdbcTemplate.queryForObject("select count(*) from CAMEL_MESSAGEPROCESSED", Integer.class));
        assertEquals("DONE-D", consumer.receiveBody("activemq2:queue:outbox", 3000));
        assertEquals("DONE-E", consumer.receiveBody("activemq2:queue:outbox", 3000));
    }

    @Test
    public void testRetryAfterException() throws Exception {
        checkInitialState();

        final AtomicInteger counter = new AtomicInteger();

        // use a notify to know when the message is done
        NotifyBuilder notify = new NotifyBuilder(context).whenDone(4).create();

        // use mock during testing as well
        getMockEndpoint("mock:a").expectedMessageCount(4);
        // there should be 1 duplicate
        getMockEndpoint("mock:b").expectedMessageCount(4);
        getMockEndpoint("mock:b").whenAnyExchangeReceived(new Processor() {
            @Override
            public void process(Exchange exchange) throws Exception {
                if (counter.getAndIncrement() == 1) {
                    throw new ConnectException("Forced cannot send to AMQ queue");
                }
            }
        });

        template.sendBodyAndHeader("activemq2:queue:inbox", "D", "uid", 111);
        template.sendBodyAndHeader("activemq2:queue:inbox", "E", "uid", 222);
        template.sendBodyAndHeader("activemq2:queue:inbox", "F", "uid", 333);

        // assert mock and wait for the message to be done
        assertMockEndpointsSatisfied();
        assertTrue("Should complete 4 messages", notify.matchesMockWaitTime());

        // check that there is two messages in the database and JMS queue
        assertEquals(new Integer(3), jdbcTemplate.queryForObject("select count(*) from CAMEL_MESSAGEPROCESSED", Integer.class));
        assertEquals("DONE-D", consumer.receiveBody("activemq2:queue:outbox", 3000));
        assertEquals("DONE-E", consumer.receiveBody("activemq2:queue:outbox", 3000));
        assertEquals("DONE-F", consumer.receiveBody("activemq2:queue:outbox", 3000));
    }

    protected void checkInitialState() {
        // check there are no messages in the database and JMS queue
        assertEquals(new Integer(0), jdbcTemplate.queryForObject("select count(*) from CAMEL_MESSAGEPROCESSED", Integer.class));
        assertNull(consumer.receiveBody("activemq2:queue:outbox", 2000));
    }

    @Override
    protected RouteBuilder createRouteBuilder() throws Exception {
        return new RouteBuilder() {
            @Override
            public void configure() throws Exception {
                IdempotentRepository<?> repository = context.getRegistry().lookupByNameAndType("messageIdRepository", IdempotentRepository.class);

                from("activemq2:queue:inbox")
                    .transacted("required")
                    .to("mock:a")
                    .idempotentConsumer(header("uid"), repository)
                    .to("mock:b")
                    .transform(simple("DONE-${body}"))
                    .to("activemq2:queue:outbox");
            }
        };
    }
}
