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

import static org.eclipse.microprofile.fault.tolerance.tck.util.TCKConfig.getConfig;

/**
 * Test that annotations can be disabled globally, then enabled at the class level and then disabled at the method level.
 *
 * The test assumes that the container supports both the MicroProfile Configuration API and the MicroProfile
 * Fault Tolerance API. All Fault tolerance policies are disabled through configuration in the deployment.
 *
 * @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 DisableAnnotationGloballyEnableOnClassDisableOnMethod extends Arquillian {

    @Inject
    private DisableAnnotationClient disableClient;

    @Deployment
    public static WebArchive deploy() {
        
        final Asset config = new DisableConfigAsset()
                .disable(Retry.class)
                .disable(CircuitBreaker.class)
                .disable(Timeout.class)
                .disable(Asynchronous.class)
                .disable(Fallback.class)
                .disable(Bulkhead.class)
                .enable(DisableAnnotationClient.class, Retry.class)
                .enable(DisableAnnotationClient.class, CircuitBreaker.class)
                .enable(DisableAnnotationClient.class, Timeout.class)
                .enable(DisableAnnotationClient.class, Asynchronous.class)
                .enable(DisableAnnotationClient.class, Fallback.class)
                .enable(DisableAnnotationClient.class, Bulkhead.class)
                .disable(DisableAnnotationClient.class, "failAndRetryOnce", Retry.class)
                .disable(DisableAnnotationClient.class, "failWithCircuitBreaker", CircuitBreaker.class)
                .disable(DisableAnnotationClient.class, "failWithTimeout", Timeout.class)
                .disable(DisableAnnotationClient.class, "asyncWaitThenReturn", Asynchronous.class)
                .disable(DisableAnnotationClient.class, "failRetryOnceThenFallback", Fallback.class)
                .disable(DisableAnnotationClient.class, "waitWithBulkhead", Bulkhead.class);

        final ConfigAnnotationAsset mpAnnotationConfig = new ConfigAnnotationAsset()
            .setValue(DisableAnnotationClient.class,"failWithTimeout",Timeout.class,getConfig().getTimeoutInStr(500))
            .mergeProperties(((DisableConfigAsset) config).getProps());

        JavaArchive testJar = ShrinkWrap
            .create(JavaArchive.class, "ftDisableGloballyEnableClassDisableMethod.jar")
            .addClasses(DisableAnnotationClient.class)
            .addPackage(Packages.UTILS)
            .addAsManifestResource(mpAnnotationConfig, "microprofile-config.properties")
            .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")
            .as(JavaArchive.class);

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

    /**
     * failAndRetryOnce is annotated with maxRetries = 1 so it is expected to execute 2 times but as Retry is disabled,
     * then no retries should be attempted.
     */
    @Test
    public void testRetryDisabled() {
        Assert.assertThrows(TestException.class, () -> disableClient.failAndRetryOnce());
        Assert.assertEquals(disableClient.getFailAndRetryOnceCounter(), 1, "Retry disabled - should be 1 exection");
    }

    /**
     * Test that a Fallback service is ignored when service fails.
     * 
     * Retry is enabled at the class level and not disabled for this method so we expect to get two executions
     */
    @Test
    public void testFallbackDisabled() {
        // Throw TestException because Fallback is disabled
        Assert.assertThrows(TestException.class, () -> disableClient.failRetryOnceThenFallback());
        // One execution because Retry is enabled
        Assert.assertEquals(disableClient.getFailRetryOnceThenFallbackCounter(), 2, "Retry enabled - should be 2 executions");
    }

    /**
     * CircuitBreaker policy being disabled the policy shouldn't be applied
     */
    @Test
    public void testCircuitBreaker() {
        // Always get TestException on first execution
        Assert.assertThrows(TestException.class, () -> disableClient.failWithCircuitBreaker());
        // Should get TestException on second execution because CircuitBreaker is disabled
        Assert.assertThrows(TestException.class, () -> disableClient.failWithCircuitBreaker());
    }

    /**
     * Test Timeout is disabled, should wait two seconds and then get a TestException
     */
    @Test
    public void testTimeout() {
        // Expect TestException because Timeout is disabled and will not fire
        Assert.assertThrows(TestException.class, () -> disableClient.failWithTimeout());
    }

    /**
     * A test to check that asynchronous is disabled
     *
     * In normal operation, asyncClient.asyncWaitThenReturn() is launched asynchronously. As Asynchronous operation was disabled via config,
     * test is expecting a synchronous operation.
     *
     * @throws InterruptedException interrupted
     * @throws ExecutionException task was aborted
     */
    @Test
    public void testAsync() throws InterruptedException, ExecutionException {
        Future<?> result = disableClient.asyncWaitThenReturn();
        try {
            Assert.assertTrue(result.isDone(), "Returned future.isDone() expected true because Async disabled");
        }
        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 disabled on the method so no exception expected
            disableClient.waitWithBulkhead(CompletableFuture.completedFuture(null));
        }
        finally {
            // Clean up executor and first two executions
            executor.shutdown();
            
            waitingFuture.complete(null);
            result1.get();
            result2.get();
        }
    }
}