/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package org.jboss.as.test.integration.ejb.singleton.concurrency;

import static org.jboss.as.controller.client.helpers.ClientConstants.OUTCOME;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.jboss.logging.Logger;
import jakarta.ejb.ConcurrentAccessTimeoutException;
import jakarta.ejb.EJB;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.as.arquillian.api.ServerSetup;
import org.jboss.as.arquillian.api.ServerSetupTask;
import org.jboss.as.arquillian.container.ManagementClient;
import org.jboss.as.controller.client.helpers.ClientConstants;
import org.jboss.dmr.ModelNode;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Testcase for testing the basic functionality of an EJB3 singleton session bean.
 *
 * @author Jaikiran Pai
 */
@RunWith(Arquillian.class)
@ServerSetup(SingletonBeanTestCase.AllowPropertyReplacementSetup.class)
public class SingletonBeanTestCase {

    private static final Logger log = Logger.getLogger(SingletonBeanTestCase.class.getName());

    @Deployment
    public static JavaArchive createDeployment() {
        // create the ejb jar
        final JavaArchive jar = ShrinkWrap.create(JavaArchive.class, "ejb3-singleton-bean-example.jar");
        jar.addClass(ReadOnlySingletonBean.class);
        jar.addClass(LongWritesSingletonBean.class);
        jar.addClass(ReadOnlySingleton.class);
        jar.addClass(SingletonBeanTestCase.class);
        jar.addClass(ReadOnlySingletonBeanDescriptor.class);
        jar.addClass(ReadOnlySingletonBeanDescriptorWithExpression.class);
        jar.addAsManifestResource(SingletonBeanTestCase.class.getPackage(), "ejb-jar.xml", "ejb-jar.xml");
        jar.addAsManifestResource(SingletonBeanTestCase.class.getPackage(), "jboss.properties", "jboss.properties");
        return jar;
    }

    static class AllowPropertyReplacementSetup implements ServerSetupTask {

        @Override
        public void setup(ManagementClient managementClient, String s) throws Exception {
            final ModelNode enableSubstitutionOp = new ModelNode();
            enableSubstitutionOp.get(ClientConstants.OP_ADDR).set(ClientConstants.SUBSYSTEM, "ee");
            enableSubstitutionOp.get(ClientConstants.OP).set(ClientConstants.WRITE_ATTRIBUTE_OPERATION);
            enableSubstitutionOp.get(ClientConstants.NAME).set("spec-descriptor-property-replacement");
            enableSubstitutionOp.get(ClientConstants.VALUE).set(true);

            try {
                applyUpdate(managementClient, enableSubstitutionOp);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public void tearDown(ManagementClient managementClient, String s) throws Exception {
            final ModelNode disableSubstitution = new ModelNode();
            disableSubstitution.get(ClientConstants.OP_ADDR).set(ClientConstants.SUBSYSTEM, "ee");
            disableSubstitution.get(ClientConstants.OP).set(ClientConstants.WRITE_ATTRIBUTE_OPERATION);
            disableSubstitution.get(ClientConstants.NAME).set("spec-descriptor-property-replacement");
            disableSubstitution.get(ClientConstants.VALUE).set(false);

            try {
                applyUpdate(managementClient, disableSubstitution);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private void applyUpdate(final ManagementClient managementClient, final ModelNode update)
                throws Exception {
            ModelNode result = managementClient.getControllerClient().execute(update);
            if (result.hasDefined(OUTCOME)
                    && ClientConstants.SUCCESS.equals(result.get(OUTCOME).asString())) {
            } else if (result.hasDefined(ClientConstants.FAILURE_DESCRIPTION)) {
                final String failureDesc = result.get(ClientConstants.FAILURE_DESCRIPTION).toString();
                throw new RuntimeException(failureDesc);
            } else {
                throw new RuntimeException("Operation not successful; outcome = " + result.get("outcome"));
            }
        }
    }

    @EJB(mappedName = "java:global/ejb3-singleton-bean-example/ReadOnlySingletonBean!org.jboss.as.test.integration.ejb.singleton.concurrency.ReadOnlySingletonBean")
    private ReadOnlySingletonBean readOnlySingletonBean;

    @EJB(mappedName = "java:global/ejb3-singleton-bean-example/LongWritesSingletonBean!org.jboss.as.test.integration.ejb.singleton.concurrency.LongWritesSingletonBean")
    private LongWritesSingletonBean longWritesSingletonBean;

    @EJB(mappedName = "java:global/ejb3-singleton-bean-example/ReadOnlySingletonBeanDescriptor!org.jboss.as.test.integration.ejb.singleton.concurrency.ReadOnlySingletonBeanDescriptor")
    private ReadOnlySingletonBeanDescriptor readOnlySingletonBeanDescriptor;

    @EJB(mappedName = "java:global/ejb3-singleton-bean-example/ReadOnlySingletonBeanDescriptorWithExpression!org.jboss.as.test.integration.ejb.singleton.concurrency.ReadOnlySingletonBeanDescriptorWithExpression")
    private ReadOnlySingletonBeanDescriptorWithExpression readOnlySingletonBeanDescriptorWithExpression;


    /**
     * Tests that the concurrency on a singleton bean with lock type READ works as expected
     *
     * @throws Exception
     */
    @Test
    public void testReadOnlySingletonBean() throws Exception {
        testReadOnlySingleton(readOnlySingletonBean);
    }

    @Test
    public void testReadOnlySingletonDescriptor() throws Exception {
        testReadOnlySingleton(readOnlySingletonBeanDescriptor);
    }

    @Test
    public void testReadOnlySingletonDescriptorWithExpression() throws Exception {
        testReadOnlySingleton(readOnlySingletonBeanDescriptorWithExpression);
    }

    public void testReadOnlySingleton(ReadOnlySingleton readOnlySingleton) throws Exception {
        final int NUM_THREADS = 10;
        ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
        @SuppressWarnings("unchecked")
        Future<String>[] results = new Future[NUM_THREADS];
        for (int i = 0; i < NUM_THREADS; i++) {
            results[i] = executor.submit(new ReadOnlySingletonBeanInvoker(readOnlySingleton, i));
        }
        for (int i = 0; i < NUM_THREADS; i++) {
            String result = results[i].get(10, TimeUnit.SECONDS);
            Assert.assertEquals("Unexpected value from singleton bean", String.valueOf(i), result);
        }
    }

    /**
     * Tests that invocation on a singleton bean method with write lock results in ConcurrentAccessTimeoutException
     * for subsequent invocations, if the previous invocation(s) hasn't yet completed.
     *
     * @throws Exception
     */
    @Test
    public void testLongWritesSingleton() throws Exception {

        // let's invoke a bean method (with WRITE lock semantics) which takes a long time to complete
        final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        // let's now try and invoke on this bean while the previous operation is in progress.
        // we expect a ConcurrentAccessTimeoutException
        final int NUM_THREADS = 10;
        final ExecutorService nextTenInvocations = Executors.newFixedThreadPool(NUM_THREADS);
        Future<?>[] results = new Future[NUM_THREADS];
        // let the 10 threads invoke on the bean's method (which has WRITE lock semantics) which has an accesstimeout value
        // set on it
        for (int i = 0; i < NUM_THREADS; i++) {
            results[i] = nextTenInvocations.submit(new LongWritesSingletonBeanInvoker(longWritesSingletonBean));
        }
        // Now fetch the results.
        // all are expected to timeout

        // one is expected to complete successfully
        // rest all are expected to timeout
        final List<Object> passed = new ArrayList<Object>();
        final List<Throwable> throwables = new ArrayList<Throwable>();
        for (int i = 0; i < NUM_THREADS; i++) {
            try {
                passed.add(results[i].get(10, TimeUnit.SECONDS));
            } catch (ExecutionException ee) {
                throwables.add(ee.getCause());
            }
        }
        // only one call succeeded, so count should be 1
        Assert.assertEquals("Unexpected count on singleton bean after invocation on method with WRITE lock semantic: ", 1, this.longWritesSingletonBean.getCount());

        for (Throwable t : throwables) {
            assertTrue(t.toString(), t instanceof ConcurrentAccessTimeoutException);
        }

        assertEquals(1, passed.size());
        assertEquals(NUM_THREADS - 1, throwables.size());
    }

    private class ReadOnlySingletonBeanInvoker implements Callable<String> {

        private ReadOnlySingleton bean;

        private int num;

        ReadOnlySingletonBeanInvoker(ReadOnlySingleton bean, int num) {
            this.bean = bean;
            this.num = num;
        }

        @Override
        public String call() throws Exception {
            log.trace("Bean: " + bean.toString());
            return bean.twoSecondEcho(String.valueOf(this.num));
        }
    }

    private class LongWritesSingletonBeanInvoker implements Callable<Object> {

        private LongWritesSingletonBean bean;

        LongWritesSingletonBeanInvoker(LongWritesSingletonBean bean) {
            this.bean = bean;
        }

        @Override
        public Object call() throws Exception {
            bean.fiveSecondWriteOperation();
            return null;
        }
    }
}
