/*
 *******************************************************************************
 * Copyright (c) 2016-2018 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * Licensed 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.eclipse.microprofile.fault.tolerance.tck.disableEnv;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import javax.inject.Inject;

import org.eclipse.microprofile.fault.tolerance.tck.util.Packages;
import org.eclipse.microprofile.fault.tolerance.tck.util.TestException;
import org.eclipse.microprofile.faulttolerance.Asynchronous;
import org.eclipse.microprofile.faulttolerance.Bulkhead;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
import org.eclipse.microprofile.faulttolerance.Fallback;
import org.eclipse.microprofile.faulttolerance.Retry;
import org.eclipse.microprofile.faulttolerance.Timeout;
import org.eclipse.microprofile.faulttolerance.exceptions.BulkheadException;
import org.eclipse.microprofile.faulttolerance.exceptions.CircuitBreakerOpenException;
import org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.testng.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.Asset;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.testng.Assert;
import org.testng.annotations.Test;

/**
 * Test that annotations can be disabled at the class level and then re-enabled at the method level.
 * 
 * @author <a href="mailto:[email protected]">Antoine Sabot-Durand</a>
 * @author <a href="mailto:[email protected]">Neil Young</a>
 * @author <a href="mailto:[email protected]">Andrew Rouse</a>
 */
public class DisableAnnotationOnClassEnableOnMethodTest extends Arquillian {

    @Inject
    private DisableAnnotationClient disableClient;

    @Deployment
    public static WebArchive deploy() {
       Asset config = new DisableConfigAsset()
               .disable(DisableAnnotationClient.class, Retry.class)
               .disable(DisableAnnotationClient.class, CircuitBreaker.class)
               .disable(DisableAnnotationClient.class, Timeout.class)
               .disable(DisableAnnotationClient.class, Asynchronous.class)
               .disable(DisableAnnotationClient.class, Fallback.class)
               .disable(DisableAnnotationClient.class, Bulkhead.class)
               .enable(DisableAnnotationClient.class, "failAndRetryOnce", Retry.class)
               .enable(DisableAnnotationClient.class, "failWithCircuitBreaker", CircuitBreaker.class)
               .enable(DisableAnnotationClient.class, "failWithTimeout", Timeout.class)
               .enable(DisableAnnotationClient.class, "asyncWaitThenReturn", Asynchronous.class)
               .enable(DisableAnnotationClient.class, "failRetryOnceThenFallback", Fallback.class)
               .enable(DisableAnnotationClient.class, "waitWithBulkhead", Bulkhead.class);
        
        JavaArchive testJar = ShrinkWrap
            .create(JavaArchive.class, "ftDisableClassEnableMethod.jar")
            .addClasses(DisableAnnotationClient.class)
            .addPackage(Packages.UTILS)
            .addAsManifestResource(config, "microprofile-config.properties")
            .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")
            .as(JavaArchive.class);

        WebArchive war = ShrinkWrap
            .create(WebArchive.class, "ftDisableClassEnableMethod.war")
            .addAsLibrary(testJar);
        return war;
    }

    /**
     * failAndRetryOnce is annotated with maxRetries = 1 so it is expected to execute 2 times.
     */
    @Test
    public void testRetryEnabled() {
        // Always get a TestException
        Assert.assertThrows(TestException.class, () -> disableClient.failAndRetryOnce());
        // Should get two attempts if retry is enabled
        Assert.assertEquals(disableClient.getFailAndRetryOnceCounter(), 2, "Retry enabled - should be 2 exections");
    }

    /**
     * Test that a Fallback service is used when service fails.
     *
     * Retry has been disabled on the class and has not been enabled for the method,
     * therefore there should only be one execution
     */
    @Test
    public void testFallbackDisabled() {
        // Expect no exception because fallback is enabled
        disableClient.failRetryOnceThenFallback();
        // One execution because Retry is disabled
        Assert.assertEquals(disableClient.getFailRetryOnceThenFallbackCounter(), 1, "Retry disabled - should be 1 execution");
    }

    /**
     * CircuitBreaker is enabled on the method so the policy should be applied
     */
    @Test
    public void testCircuitBreaker() {
        // Always get TestException on first execution
        Assert.assertThrows(TestException.class, () -> disableClient.failWithCircuitBreaker());
        // Should get CircuitBreakerOpenException on second execution because CircuitBreaker is enabled
        Assert.assertThrows(CircuitBreakerOpenException.class, () -> disableClient.failWithCircuitBreaker());
    }

    /**
     * Test Timeout is enabled, should fail with a timeout exception
     */
    @Test
    public void testTimeout() {
        // Expect TimeoutException because Timeout is enabled and method will time out
        Assert.assertThrows(TimeoutException.class, () -> disableClient.failWithTimeout());
    }

    /**
     * A test to check that asynchronous is enabled
     *
     * @throws InterruptedException interrupted
     * @throws ExecutionException task was aborted
     */
    @Test
    public void testAsync() throws InterruptedException, ExecutionException {
        Future<?> result = disableClient.asyncWaitThenReturn();
        try {
            Assert.assertFalse(result.isDone(), "Returned future.isDone() expected false because Async enabled");
        }
        finally {
            result.get(); // Success or failure, don't leave the future lying around
        }
    }
    
    /**
     * Test whether Bulkhead is enabled on {@code waitWithBulkhead()}
     *
     * @throws InterruptedException interrupted
     * @throws ExecutionException task was aborted
     */
    @Test
    public void testBulkhead() throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        
        // Start two executions at once
        CompletableFuture<Void> waitingFuture = new CompletableFuture<>();
        Future<?> result1 = executor.submit(() -> disableClient.waitWithBulkhead(waitingFuture));
        Future<?> result2 = executor.submit(() -> disableClient.waitWithBulkhead(waitingFuture));
        
        try {
            disableClient.waitForBulkheadExecutions(2);
            
            // Try to start a third execution. This would throw a BulkheadException if Bulkhead is enabled.
            // Bulkhead is enabled on the method, so expect exception
            Assert.assertThrows(BulkheadException.class, () -> disableClient.waitWithBulkhead(CompletableFuture.completedFuture(null)));
        }
        finally {
            // Clean up executor and first two executions
            executor.shutdown();
            
            waitingFuture.complete(null);
            result1.get();
            result2.get();
        }
    }
}