package org.jboss.resteasy.test.resource.param;

import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.core.Response;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.resteasy.client.jaxrs.ProxyBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.spi.HttpResponseCodes;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResource;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceArray;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceArrayDefault;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceArrayDefaultNull;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceArrayDefaultOverride;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceDefault;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceDefaultNull;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceDefaultOverride;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceList;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceListDefault;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceListDefaultNull;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceListDefaultOverride;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceQueryPrimitivesInterface;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceResourceArray;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceResourceListInterface;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceResourceWrappersInterface;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceWrappers;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceWrappersDefault;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceWrappersDefaultNull;
import org.jboss.resteasy.test.resource.param.resource.QueryParamAsPrimitiveResourceWrappersDefaultOverride;
import org.jboss.resteasy.util.HttpHeaderNames;
import org.jboss.resteasy.utils.PortProviderUtil;
import org.jboss.resteasy.utils.TestUtil;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

/**
 * @tpSubChapter Parameters
 * @tpChapter Integration tests
 * @tpTestCaseDetails Test all variants of primitive query parameters (boolean, int, long, float, etc.)
 * @tpSince RESTEasy 3.0.16
 */
@ExtendWith(ArquillianExtension.class)
@RunAsClient
public class QueryParamAsPrimitiveTest {

    public static final String ERROR_MESSAGE = "Wrong object received";

    private static QueryParamAsPrimitiveResourceQueryPrimitivesInterface resourceQueryPrimitives;

    private static QueryParamAsPrimitiveResourceResourceWrappersInterface resourceQueryPrimitiveWrappers;

    private static QueryParamAsPrimitiveResourceResourceListInterface resourceQueryPrimitiveList;

    private static QueryParamAsPrimitiveResourceResourceArray resourceQueryPrimitiveArray;
    private static ResteasyClient client;

    @Deployment
    public static Archive<?> deploy() throws Exception {
        WebArchive war = TestUtil.prepareArchive(QueryParamAsPrimitiveTest.class.getSimpleName());
        war.addClass(QueryParamAsPrimitiveResourceQueryPrimitivesInterface.class);
        war.addClass(QueryParamAsPrimitiveResourceResourceArray.class);
        war.addClass(QueryParamAsPrimitiveResourceResourceListInterface.class);
        war.addClass(QueryParamAsPrimitiveResourceResourceWrappersInterface.class);
        war.addClass(QueryParamAsPrimitiveTest.class);
        return TestUtil.finishContainerPrepare(war, null,
                QueryParamAsPrimitiveResource.class,
                QueryParamAsPrimitiveResourceDefault.class,
                QueryParamAsPrimitiveResourceDefaultOverride.class,
                QueryParamAsPrimitiveResourceDefaultNull.class,
                QueryParamAsPrimitiveResourceWrappers.class,
                QueryParamAsPrimitiveResourceWrappersDefault.class,
                QueryParamAsPrimitiveResourceWrappersDefaultNull.class,
                QueryParamAsPrimitiveResourceWrappersDefaultOverride.class,
                QueryParamAsPrimitiveResourceList.class,
                QueryParamAsPrimitiveResourceListDefault.class,
                QueryParamAsPrimitiveResourceListDefaultNull.class,
                QueryParamAsPrimitiveResourceListDefaultOverride.class,
                QueryParamAsPrimitiveResourceArray.class,
                QueryParamAsPrimitiveResourceArrayDefault.class,
                QueryParamAsPrimitiveResourceArrayDefaultNull.class,
                QueryParamAsPrimitiveResourceArrayDefaultOverride.class);
    }

    @BeforeAll
    public static void before() throws Exception {
        client = (ResteasyClient) ClientBuilder.newClient();
        resourceQueryPrimitives = ProxyBuilder
                .builder(QueryParamAsPrimitiveResourceQueryPrimitivesInterface.class, client.target(generateBaseUrl())).build();
        resourceQueryPrimitiveWrappers = ProxyBuilder
                .builder(QueryParamAsPrimitiveResourceResourceWrappersInterface.class, client.target(generateBaseUrl()))
                .build();
        resourceQueryPrimitiveList = ProxyBuilder
                .builder(QueryParamAsPrimitiveResourceResourceListInterface.class, client.target(generateBaseUrl())).build();
        resourceQueryPrimitiveArray = ProxyBuilder
                .builder(QueryParamAsPrimitiveResourceResourceArray.class, client.target(generateBaseUrl())).build();
    }

    @AfterAll
    public static void after() throws Exception {
        client.close();
    }

    private static String generateURL(String path) {
        return PortProviderUtil.generateURL(path, QueryParamAsPrimitiveTest.class.getSimpleName());
    }

    private static String generateBaseUrl() {
        return PortProviderUtil.generateBaseUrl(QueryParamAsPrimitiveTest.class.getSimpleName());
    }

    public void basicTest(String type, String value) {
        String param = type + "=" + value;

        {
            String uri = updateQuery(generateURL("/"), param);
            Invocation.Builder request = client.target(uri).request();
            request.header(HttpHeaderNames.ACCEPT, "application/" + type);
            try {
                Response response = request.get();
                Assertions.assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
                response.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        {
            String uri = updateQuery(generateURL("/wrappers"), param);
            Invocation.Builder request = client.target(uri).request();
            request.header(HttpHeaderNames.ACCEPT, "application/" + type);
            try {
                Response response = request.get();
                Assertions.assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
                response.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        {
            String uri = updateQuery(generateURL("/list"), param + "&" + param + "&" + param);
            Invocation.Builder request = client.target(uri).request();
            request.header(HttpHeaderNames.ACCEPT, "application/" + type);
            try {
                Response response = request.get();
                Assertions.assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
                response.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        {
            String uri = updateQuery(generateURL("/array"), param + "&" + param + "&" + param);
            Invocation.Builder request = client.target(uri).request();
            request.header(HttpHeaderNames.ACCEPT, "application/" + type);
            try {
                Response response = request.get();
                Assertions.assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
                response.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public void testDefault(String base, String type, String value) {
        {
            Invocation.Builder request = client.target(generateURL("" + base + "default/null")).request();
            request.header(HttpHeaderNames.ACCEPT, "application/" + type);
            try {
                Response response = request.get();
                Assertions.assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
                response.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        {
            Invocation.Builder request = client.target(generateURL("" + base + "default")).request();
            request.header(HttpHeaderNames.ACCEPT, "application/" + type);
            try {
                Response response = request.get();
                Assertions.assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
                response.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        String param = type + "=" + value;
        {
            String uri = updateQuery(generateURL("" + base + "default/override"), param);
            Invocation.Builder request = client.target(uri).request();
            request.header(HttpHeaderNames.ACCEPT, "application/" + type);
            try {
                Response response = request.get();
                Assertions.assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
                response.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public void testDefault(String type, String value) {
        testDefault("/", type, value);
    }

    public void testWrappersDefault(String type, String value) {
        testDefault("/wrappers/", type, value);
    }

    public void testWrappersDefaultNull(String type) {
        Invocation.Builder request = client.target(generateURL("/wrappers/default/null")).request();
        request.header(HttpHeaderNames.ACCEPT, "application/" + type);
        try {
            Response response = request.get();
            Assertions.assertEquals(HttpResponseCodes.SC_OK, response.getStatus());
            response.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void testListDefault(String type, String value) {
        testDefault("/list/", type, value);
    }

    public void testArrayDefault(String type, String value) {
        testDefault("/array/", type, value);
    }

    /**
     * @tpTestDetails Test boolean primitive object for get request
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetBoolean() {
        basicTest("boolean", "true");
        resourceQueryPrimitives.doGet(true);
        resourceQueryPrimitiveWrappers.doGet(true);
        List<Boolean> list = new ArrayList<Boolean>();
        list.add(Boolean.TRUE);
        list.add(Boolean.TRUE);
        list.add(Boolean.TRUE);
        resourceQueryPrimitiveList.doGetBoolean(list);
        boolean[] array = { true, true, true };
        resourceQueryPrimitiveArray.doGetBoolean(array);
    }

    /**
     * @tpTestDetails Test boolean primitive object
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetBooleanPrimitivesDefault() {
        testDefault("boolean", "true");
    }

    /**
     * @tpTestDetails Test boolean primitive object with proxy
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetBooleanPrimitiveWrapperDefault() {
        testWrappersDefault("boolean", "true");
    }

    /**
     * @tpTestDetails Test boolean primitive object missing value
     * @tpSince RESTEasy 6.3.0
     */
    @Test
    public void testGetBooleanPrimitivesMissingValue() {
        testWrappersDefaultNull("boolean");
    }

    /**
     * @tpTestDetails Test boolean primitive objects in list
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetBooleanPrimitiveListDefault() {
        testListDefault("boolean", "true");
    }

    /**
     * @tpTestDetails Test boolean primitive objects in array
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetBooleanPrimitiveArrayDefault() {
        testArrayDefault("boolean", "true");
    }

    /**
     * @tpTestDetails Test byte primitive object for get method
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetByte() {
        basicTest("byte", "127");
        resourceQueryPrimitives.doGet((byte) 127);
        resourceQueryPrimitiveWrappers.doGet((byte) 127);
        List<Byte> list = new ArrayList<Byte>();
        list.add((byte) 127);
        list.add((byte) 127);
        list.add((byte) 127);
        resourceQueryPrimitiveList.doGetByte(list);
        byte[] array = { (byte) 127, (byte) 127, (byte) 127 };
        resourceQueryPrimitiveArray.doGetByte(array);
    }

    @Test()
    public void testProxyClientGetByte() {
        Assertions.assertTimeout(Duration.ofMillis(5000),
                () -> {
                    final int size = 30000;
                    byte[] array = new byte[size];
                    //Test bigger sizes
                    for (int i = 0; i < size; i++) {
                        array[i] = (byte) 127;
                    }

                    for (int i = 0; i < 5; i++) {
                        try {
                            resourceQueryPrimitiveArray.doPostByte(array);
                        } catch (BadRequestException | ProcessingException e) {
                            // expected
                        }
                    }
                });
    }

    /**
     * @tpTestDetails Test byte primitive object
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetBytePrimitivesDefault() {
        testDefault("byte", "127");
    }

    /**
     * @tpTestDetails Test byte primitive object with proxy
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetBytePrimitiveWrappersDefault() {
        testWrappersDefault("byte", "127");
    }

    /**
     * @tpTestDetails Test byte primitive object missing value
     * @tpSince RESTEasy 6.3.0
     */
    @Test
    public void testGetBytePrimitivesMissingValue() {
        testWrappersDefaultNull("byte");
    }

    /**
     * @tpTestDetails Test byte primitive objects in list
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetBytePrimitiveListDefault() {
        testListDefault("byte", "127");
    }

    /**
     * @tpTestDetails Test byte primitive objects in array
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetBytePrimitiveArrayDefault() {
        testArrayDefault("byte", "127");
    }

    /**
     * @tpTestDetails Test short primitive object for get method
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetShort() {
        basicTest("short", "32767");
    }

    /**
     * @tpTestDetails Test short primitive object
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetShortPrimitivesDefault() {
        testDefault("short", "32767");
    }

    /**
     * @tpTestDetails Test short primitive object missing value
     * @tpSince RESTEasy 6.3.0
     */
    @Test
    public void testGetShortPrimitivesMissingValue() {
        testWrappersDefaultNull("short");
    }

    /**
     * @tpTestDetails Test short primitive object with proxy
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetShortPrimitiveWrappersDefault() {
        testWrappersDefault("short", "32767");
    }

    /**
     * @tpTestDetails Test short primitive objects in list
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetShortPrimitiveListDefault() {
        testListDefault("short", "32767");
    }

    /**
     * @tpTestDetails Test int primitive object for get method
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetInt() {
        basicTest("int", "2147483647");
    }

    /**
     * @tpTestDetails Test int primitive object
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetIntPrimitivesDefault() {
        testDefault("int", "2147483647");
    }

    /**
     * @tpTestDetails Test int primitive object with proxy
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetIntPrimitiveWrappersDefault() {
        testWrappersDefault("int", "2147483647");
    }

    /**
     * @tpTestDetails Test int primitive object missing value
     * @tpSince RESTEasy 6.3.0
     */
    @Test
    public void testGetIntPrimitivesMissingValue() {
        testWrappersDefaultNull("int");
    }

    /**
     * @tpTestDetails Test int primitive objects in list
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetIntPrimitiveListDefault() {
        testListDefault("int", "2147483647");
    }

    /**
     * @tpTestDetails Test long primitive object with get method
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetLong() {
        basicTest("long", "9223372036854775807");
    }

    /**
     * @tpTestDetails Test long primitive object
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetLongPrimitivesDefault() {
        testDefault("long", "9223372036854775807");
    }

    /**
     * @tpTestDetails Test long primitive object with proxy
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetLongPrimitiveWrappersDefault() {
        testWrappersDefault("long", "9223372036854775807");
    }

    /**
     * @tpTestDetails Test long primitive object missing value
     * @tpSince RESTEasy 6.3.0
     */
    @Test
    public void testGetLongPrimitivesMissingValue() {
        testWrappersDefaultNull("long");
    }

    /**
     * @tpTestDetails Test long primitive objects in list
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetLongPrimitiveListDefault() {
        testListDefault("long", "9223372036854775807");
    }

    /**
     * @tpTestDetails Test float primitive object with get method
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetFloat() {
        basicTest("float", "3.14159265");
    }

    /**
     * @tpTestDetails Test float primitive object
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetFloatPrimitivesDefault() {
        testDefault("float", "3.14159265");
    }

    /**
     * @tpTestDetails Test float primitive object with proxy
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetFloatPrimitiveWrappersDefault() {
        testWrappersDefault("float", "3.14159265");
    }

    /**
     * @tpTestDetails Test float primitive object missing value
     * @tpSince RESTEasy 6.3.0
     */
    @Test
    public void testGetFloatPrimitivesMissingValue() {
        testWrappersDefaultNull("float");
    }

    /**
     * @tpTestDetails Test float primitive objects in list
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetFloatPrimitiveListDefault() {
        testListDefault("float", "3.14159265");
    }

    /**
     * @tpTestDetails Test double primitive object with get method
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetDouble() {
        basicTest("double", "3.14159265358979");
    }

    /**
     * @tpTestDetails Test double primitive object
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetDoublePrimitivesDefault() {
        testDefault("double", "3.14159265358979");
    }

    /**
     * @tpTestDetails Test double primitive object with proxy
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetDoublePrimitiveWrappersDefault() {
        testWrappersDefault("double", "3.14159265358979");
    }

    /**
     * @tpTestDetails Test double primitive object missing value
     * @tpSince RESTEasy 6.3.0
     */
    @Test
    public void testGetDoublePrimitivesMissingValue() {
        testWrappersDefaultNull("double");
    }

    /**
     * @tpTestDetails Test double primitive objects in list
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testGetDoublePrimitiveListDefault() {
        testListDefault("double", "3.14159265358979");
    }

    /**
     * @tpTestDetails Test char primitive object for get method
     * @tpSince RESTEasy 3.0.24
     */
    @Test
    public void testGetChar() {
        basicTest("char", "a");
    }

    /**
     * @tpTestDetails Test char primitive object
     * @tpSince RESTEasy 3.0.24
     */
    @Test
    public void testGetCharPrimitivesDefault() {
        testDefault("char", "a");
    }

    /**
     * @tpTestDetails Test char primitive object with proxy
     * @tpSince RESTEasy 3.0.24
     */
    @Test
    public void testGetCharPrimitiveWrappersDefault() {
        testWrappersDefault("char", "a");
    }

    /**
     * @tpTestDetails Test char primitive object missing value
     * @tpSince RESTEasy 6.3.0
     */
    @Test
    public void testGetCharPrimitivesMissingValue() {
        testWrappersDefaultNull("char");
    }

    /**
     * @tpTestDetails Test char primitive objects in list
     * @tpSince RESTEasy 3.0.24
     */
    @Test
    public void testGetCharPrimitiveListDefault() {
        testListDefault("char", "a");
    }

    /**
     * @tpTestDetails Test char primitive objects in array
     * @tpSince RESTEasy 3.0.24
     */
    @Test
    public void testGetCharPrimitiveArrayDefault() {
        testArrayDefault("char", "a");
    }

    /**
     * @tpTestDetails Negative testing: accept only int, but string is provided. Error is excepted.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testBadPrimitiveValue() {
        {
            String uri = updateQuery(generateURL("/"), "int=abcdef");
            Invocation.Builder request = client.target(uri).request();
            request.header(HttpHeaderNames.ACCEPT, "application/int");
            try {
                Response response = request.get();
                Assertions.assertEquals(404, response.getStatus());
                response.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * @tpTestDetails Negative testing: accept only int, but string is provided. Error is excepted.
     *                Proxy is used.
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testBadPrimitiveWrapperValue() {
        {
            String uri = updateQuery(generateURL("/wrappers"), "int=abcdef");
            Invocation.Builder request = client.target(uri).request();
            request.header(HttpHeaderNames.ACCEPT, "application/int");
            try {
                Response response = request.get();
                Assertions.assertEquals(404, response.getStatus());
                response.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * @tpTestDetails Negative testing: accept only int, but string is provided. Error is excepted.
     *                List is tested
     * @tpSince RESTEasy 3.0.16
     */
    @Test
    public void testBadPrimitiveListValue() {
        {
            String uri = updateQuery(generateURL("/list"), "int=abcdef&int=abcdef");
            Invocation.Builder request = client.target(uri).request();
            request.header(HttpHeaderNames.ACCEPT, "application/int");
            try {
                Response response = request.get();
                Assertions.assertEquals(404, response.getStatus());
                response.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static String updateQuery(String uriString, String query) {
        try {
            URI uri = new URI(uriString);
            return new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), query, uri.getFragment()).toString();
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }
}
