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

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;

import org.apache.camel.CamelExecutionException;
import org.apache.camel.ContextTestSupport;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.junit.Assert;

/**
 * Unit test for Java 8 {@link CompletableFuture} as return type on a bean being called from a Camel route.
 */
public class BeanInvokeAsyncTest extends ContextTestSupport {
    
    private volatile CompletableFuture<Object> callFuture;
    private volatile String receivedBody;
    private volatile CountDownLatch methodInvoked;
    private Future<Object> sendFuture;

    public void testDoSomething() throws Exception {

        runTestSendBody("Hello World", "Hello World", this::doSomething);
        runTestSendBody("", "", this::doSomething);
        runTestSendBody(this::expectNullBody, null, this::doSomething);
    }

    public void testChangeSomething() throws Exception {
        runTestSendBody("Bye World", "Hello World", this::changeSomething);
        runTestSendBody("Bye All", null, this::changeSomething);
        runTestSendBody("Bye All", "", this::changeSomething);

    }

    public void testDoNothing() throws Exception {
        runTestSendBody("Hello World", "Hello World", this::doNothing);
        runTestSendBody("", "", this::doNothing);
        runTestSendBody(this::expectNullBody, null, this::doNothing);
    }

    public void testThrowSomething() throws Exception {
        try {
            runTestSendBody(m -> m.expectedMessageCount(0), "SomeProblem", this::throwSomething);
            fail("Exception expected");
        } catch (ExecutionException e) {
            Assert.assertTrue(e.getCause() instanceof CamelExecutionException);
            Assert.assertTrue(e.getCause().getCause() instanceof IllegalStateException);
            Assert.assertEquals("SomeProblem", e.getCause().getCause().getMessage());
        }
    }

    private void runTestSendBody(String expectedBody, String sentBody,
                                 Function<String, String> processor) throws Exception {
        runTestSendBody(m -> m.expectedBodiesReceived(expectedBody), sentBody, processor);
    }

    private void runTestSendBody(Consumer<MockEndpoint> mockPreparer, String sentBody,
                                 Function<String, String> processor) throws Exception {
        MockEndpoint mock = getMockEndpoint("mock:result");
        mock.reset();
        mockPreparer.accept(mock);

        callFuture = new CompletableFuture<>();
        methodInvoked = new CountDownLatch(1);
        sendFuture = template.asyncSendBody("direct:entry", sentBody);

        Assert.assertTrue(methodInvoked.await(5, TimeUnit.SECONDS));
        Assert.assertEquals(0, mock.getReceivedCounter());
        Assert.assertFalse(sendFuture.isDone());
        try {
            callFuture.complete(processor.apply(receivedBody));
        } catch (Exception e) {
            callFuture.completeExceptionally(e);
        }
        sendFuture.get();

        assertMockEndpointsSatisfied();
    }

    private void expectNullBody(MockEndpoint mock) {
        mock.expectedMessageCount(1);
        mock.message(0).body().isNull();
    }

    @Override
    protected RouteBuilder createRouteBuilder() throws Exception {
        return new RouteBuilder() {
            @Override
            public void configure() throws Exception {
                from("direct:entry").bean(BeanInvokeAsyncTest.this, "asyncMethod").to("mock:result");
            }
        };
    }

    // java 8 async return type
    public CompletableFuture<?> asyncMethod(String body) {
        this.receivedBody = body;
        methodInvoked.countDown();
        return callFuture;
    }

    public String doSomething(String s) {
        return s;
    }

    public String changeSomething(String s) {
        if ("Hello World".equals(s)) {
            return "Bye World";
        }
        return "Bye All";
    }

    public String doNothing(String s) {
        return null;
    }

    public String throwSomething(String s) {
        throw new IllegalStateException(s);
    }
}