package org.jboss.resteasy.test.cdi.basic;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.lang.reflect.ReflectPermission;
import java.net.SocketPermission;
import java.security.SecurityPermission;
import java.util.Hashtable;
import java.util.PropertyPermission;
import java.util.logging.LoggingPermission;

import javax.naming.Context;
import javax.naming.InitialContext;

import jakarta.inject.Inject;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpResponseCodes;
import org.jboss.resteasy.test.cdi.basic.resource.EJBApplication;
import org.jboss.resteasy.test.cdi.basic.resource.EJBBook;
import org.jboss.resteasy.test.cdi.basic.resource.EJBBookReader;
import org.jboss.resteasy.test.cdi.basic.resource.EJBBookReaderImpl;
import org.jboss.resteasy.test.cdi.basic.resource.EJBBookResource;
import org.jboss.resteasy.test.cdi.basic.resource.EJBBookWriterImpl;
import org.jboss.resteasy.test.cdi.basic.resource.EJBLocalResource;
import org.jboss.resteasy.test.cdi.basic.resource.EJBRemoteResource;
import org.jboss.resteasy.test.cdi.basic.resource.EJBResourceParent;
import org.jboss.resteasy.test.cdi.util.Constants;
import org.jboss.resteasy.test.cdi.util.Counter;
import org.jboss.resteasy.test.cdi.util.Utilities;
import org.jboss.resteasy.test.cdi.util.UtilityProducer;
import org.jboss.resteasy.utils.PermissionUtil;
import org.jboss.resteasy.utils.PortProviderUtil;
import org.jboss.resteasy.utils.TestUtil;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

/**
 * @tpSubChapter CDI
 * @tpChapter Integration tests
 * @tpTestCaseDetails EJB and RESTEasy integration test.
 * @tpSince RESTEasy 3.0.16
 */
@ExtendWith(ArquillianExtension.class)
public class EJBTest {

    private static Logger log = Logger.getLogger(EJBTest.class);

    @Inject
    EJBLocalResource localResource;

    /**
     * value of DEPLOYMENT_NAME is also used in ejbtest_web.xml file
     */
    public static final String DEPLOYMENT_NAME = "resteasy-ejb-test";

    private Client client;

    @Deployment
    public static Archive<?> createTestArchive() {
        WebArchive war = ShrinkWrap.create(WebArchive.class, DEPLOYMENT_NAME + ".war");
        // test needs to use special annotations in Application class, TestApplication class could not be used
        war.addClass(EJBApplication.class);
        war.addClass(PortProviderUtil.class);
        war.addClasses(EJBBook.class, Constants.class, Counter.class, UtilityProducer.class, Utilities.class, TestUtil.class)
                .addClasses(EJBBookReader.class, EJBBookReaderImpl.class)
                .addClasses(EJBBookWriterImpl.class)
                .addClasses(EJBResourceParent.class, EJBLocalResource.class, EJBRemoteResource.class, EJBBookResource.class)
                .setWebXML(EJBTest.class.getPackage(), "ejbtest_web.xml")
                .addAsWebInfResource(TestUtil.createBeansXml(), "beans.xml");
        // Arquillian in the deployment
        war.addAsManifestResource(PermissionUtil.createPermissionsXmlAsset(new ReflectPermission("suppressAccessChecks"),
                new LoggingPermission("control", ""),
                new PropertyPermission("arquillian.*", "read"),
                new PropertyPermission("ipv6", "read"),
                new PropertyPermission("node", "read"),
                new PropertyPermission("org.jboss.resteasy.port", "read"),
                new PropertyPermission("quarkus.tester", "read"),
                new RuntimePermission("accessDeclaredMembers"),
                new SecurityPermission("insertProvider"),
                new RuntimePermission("getenv.RESTEASY_PORT"),
                new SocketPermission(PortProviderUtil.getHost(), "connect,resolve")), "permissions.xml");
        return war;
    }

    private String generateURL(String path) {
        return PortProviderUtil.generateURL(path, DEPLOYMENT_NAME);
    }

    /**
     * client needs to be non-static. BeforeClass and AfterClass methods are not executed on server (@RunAsClient annotation is
     * not used).
     */
    @BeforeEach
    public void init() {
        client = ClientBuilder.newClient();
    }

    @AfterEach
    public void close() {
        client.close();
    }

    /**
     * @tpTestDetails Verify that EJBBookReaderImpl, EJBBookWriterImpl, and EJBBookResource
     *                are placed in the correct scope.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testVerifyScopesJaxRs() throws Exception {
        log.info("starting testVerifyScopesJaxRs()");

        WebTarget base = client.target(generateURL("/verifyScopes/"));
        Response response = base.request().get();
        assertEquals(HttpResponseCodes.SC_OK, response.getStatus(), "Wrong response status");
        assertEquals(HttpResponseCodes.SC_OK, response.readEntity(Integer.class).intValue(), "Wrong response content");
    }

    /**
     * @tpTestDetails Verify that EJBBookReaderImpl, EJBBookWriterImpl, and EJBBookResource
     *                are placed in the correct scope on local.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testVerifyScopesLocalEJB() throws Exception {
        log.info("starting testVerifyScopesLocalEJB()");
        int result = localResource.verifyScopes();
        assertEquals(HttpResponseCodes.SC_OK, result);
    }

    /**
     * @tpTestDetails Verify that EJBBookReaderImpl, EJBBookWriterImpl, and EJBBookResource
     *                are placed in the correct scope on remote.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testVerifyScopesRemoteEJB() throws Exception {
        log.info("starting testVerifyScopesRemoteEJB()");

        // Get proxy to JAX-RS resource as EJB.
        EJBRemoteResource remoteResource = getRemoteResource();
        log.info("remote: " + remoteResource);
        int result = remoteResource.verifyScopes();
        log.info("result: " + result);
        assertEquals(HttpResponseCodes.SC_OK, result);
    }

    /**
     * @tpTestDetails Verify that EJBBookReader and EJBBookWriterImpl are correctly injected
     *                into EJBBookResource.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testVerifyInjectionJaxRs() throws Exception {
        log.info("starting testVerifyInjectionJaxRs()");
        WebTarget base = client.target(generateURL("/verifyInjection/"));
        Response response = base.request().get();
        assertEquals(HttpResponseCodes.SC_OK, response.getStatus(), "Wrong response status");
        assertEquals(HttpResponseCodes.SC_OK, response.readEntity(Integer.class).intValue(), "Wrong response content");
    }

    /**
     * @tpTestDetails Verify that EJBBookReader and EJBBookWriterImpl are correctly injected
     *                into EJBBookResource.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testVerifyInjectionLocalEJB() throws Exception {
        log.info("starting testVerifyInjectionLocalEJB()");
        int result = localResource.verifyInjection();
        log.info("testVerifyInjectionLocalEJB result: " + result);
        assertEquals(HttpResponseCodes.SC_OK, result);
    }

    /**
     * @tpTestDetails Verify that EJBBookReader and EJBBookWriterImpl are correctly injected
     *                into EJBBookResource.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testVerifyInjectionRemoteEJB() throws Exception {
        log.info("starting testVerifyInjectionRemoteEJB()");

        // Get proxy to JAX-RS resource as EJB.
        EJBRemoteResource remoteResource = getRemoteResource();
        log.info("remote: " + remoteResource);
        int result = remoteResource.verifyInjection();
        log.info("result: " + result);
        assertEquals(HttpResponseCodes.SC_OK, result);
    }

    /**
     * @tpTestDetails Further addresses the use of EJBs as JAX-RS components.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testAsJaxRSResource() throws Exception {
        log.info("entering testAsJaxRSResource()");

        // Create book.
        WebTarget base = client.target(generateURL("/create/"));
        EJBBook book1 = new EJBBook("RESTEasy: the Sequel");
        Response response = base.request().post(Entity.entity(book1, Constants.MEDIA_TYPE_TEST_XML));
        log.info("Status: " + response.getStatus());
        assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
        int id1 = response.readEntity(int.class);
        log.info("id: " + id1);
        assertEquals(Counter.INITIAL_VALUE, id1, "Wrong id of Book1 id");

        // Create another book.
        EJBBook book2 = new EJBBook("RESTEasy: It's Alive");
        response = base.request().post(Entity.entity(book2, Constants.MEDIA_TYPE_TEST_XML));
        log.info("Status: " + response.getStatus());
        assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
        int id2 = response.readEntity(int.class);
        log.info("id: " + id2);
        assertEquals(Counter.INITIAL_VALUE + 1, id2, "Wrong id of Book2 id");

        // Retrieve first book.
        base = client.target(generateURL("/book/" + id1));
        response = base.request().accept(Constants.MEDIA_TYPE_TEST_XML).get();
        log.info("Status: " + response.getStatus());
        assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
        EJBBook result = response.readEntity(EJBBook.class);
        log.info("book: " + book1);
        assertEquals(book1, result, "Wrong book1 received from server");

        // Retrieve second book.
        base = client.target(generateURL("/book/" + id2));
        response = base.request().accept(Constants.MEDIA_TYPE_TEST_XML).get();
        log.info("Status: " + response.getStatus());
        assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
        result = response.readEntity(EJBBook.class);
        log.info("book: " + book2);
        assertEquals(book2, result, "Wrong book2 received from server");

        // Verify that EJBBookReader and EJBBookWriter have been used, twice on each side.
        base = client.target(generateURL("/uses/4"));
        response = base.request().get();
        log.info("Status: " + response.getStatus());
        assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
        response.close();

        // Reset counter.
        base = client.target(generateURL("/reset"));
        response = base.request().get();
        log.info("Status: " + response.getStatus());
        assertEquals(HttpResponseCodes.SC_NO_CONTENT, response.getStatus());
        response.close();
    }

    /**
     * @tpTestDetails Invokes additional methods of JAX-RS resource as local EJB.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testAsLocalEJB() throws Exception {
        log.info("entering testAsLocalEJB()");

        // Create book.
        EJBBook book1 = new EJBBook("RESTEasy: the Sequel");
        int id1 = localResource.createBook(book1);
        log.info("id1: " + id1);
        assertEquals(Counter.INITIAL_VALUE, id1, "Wrong id of Book1 id");

        // Create another book.
        EJBBook book2 = new EJBBook("RESTEasy: It's Alive");
        int id2 = localResource.createBook(book2);
        log.info("id2: " + id2);
        assertEquals(Counter.INITIAL_VALUE + 1, id2, "Wrong id of Book2 id");

        // Retrieve first book.
        EJBBook bookResponse1 = localResource.lookupBookById(id1);
        log.info("book1 response: " + bookResponse1);
        assertEquals(book1, bookResponse1, "Wrong book1 received from server");

        // Retrieve second book.
        EJBBook bookResponse2 = localResource.lookupBookById(id2);
        log.info("book2 response: " + bookResponse2);
        assertEquals(book2, bookResponse2, "Wrong book2 received from server");

        // Verify that EJBBookReader and EJBBookWriter haven't been used.
        localResource.testUse(0);

        // Reset counter.
        localResource.reset();
    }

    /**
     * @tpTestDetails Invokes additional methods of JAX-RS resource as remote EJB.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testAsRemoteEJB() throws Exception {
        log.info("entering testAsRemoteEJB()");

        // Get proxy to JAX-RS resource as EJB.
        EJBRemoteResource remoteResource = getRemoteResource();
        log.info("remote: " + remoteResource);

        // Create book.
        EJBBook book1 = new EJBBook("RESTEasy: the Sequel");
        int id1 = remoteResource.createBook(book1);
        log.info("id1: " + id1);
        assertEquals(Counter.INITIAL_VALUE, id1, "Wrong id of Book1 id");

        // Create another book.
        EJBBook book2 = new EJBBook("RESTEasy: It's Alive");
        int id2 = remoteResource.createBook(book2);
        log.info("id2: " + id2);
        assertEquals(Counter.INITIAL_VALUE + 1, id2, "Wrong id of Book2 id");

        // Retrieve first book.
        EJBBook bookResponse1 = remoteResource.lookupBookById(id1);
        log.info("book1 response: " + bookResponse1);
        assertEquals(book1, bookResponse1, "Wrong book1 received from server");

        // Retrieve second book.
        EJBBook bookResponse2 = remoteResource.lookupBookById(id2);
        log.info("book2 response: " + bookResponse2);
        assertEquals(book2, bookResponse2, "Wrong book2 received from server");

        // Verify that EJBBookReader and EJBBookWriter haven't been used.
        remoteResource.testUse(0);

        // Reset counter.
        remoteResource.reset();
    }

    private static EJBRemoteResource getRemoteResource() throws Exception {
        final Hashtable<String, String> jndiProperties = new Hashtable<>();
        jndiProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
        final Context context = new InitialContext(jndiProperties);
        String name = "ejb:/" + DEPLOYMENT_NAME + "/EJBBookResource!" + EJBRemoteResource.class.getName();
        return EJBRemoteResource.class.cast(context.lookup(name));
    }
}
