package org.jboss.resteasy.test.providers.jsonb.basic;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;

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.MediaType;
import jakarta.ws.rs.core.Response;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.container.test.api.TargetsContainer;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.logging.Logger;
import org.jboss.resteasy.test.ContainerConstants;
import org.jboss.resteasy.test.providers.jsonb.basic.resource.Cat;
import org.jboss.resteasy.test.providers.jsonb.basic.resource.JsonBindingCustomRepeaterProvider;
import org.jboss.resteasy.test.providers.jsonb.basic.resource.JsonBindingResource;
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 Json-binding provider.JAX-RS 2.1 spec (JSR-370), section 11.2.7 states,
 *               "Note that if JSON-B and JSON-P are both supported in the same environment, entity providers for
 *               JSON-B take precedence over those for JSON-P for all types except JsonValue and its sub-types."
 *               The sub-types of JsonValue are JsonArray, JsonNumber, JsonObject, JsonString, JsonStructure.
 *               Resteasy's JSON-P providers currently supports this. A general object such as Cat will be processed by
 *               the JSON-B provider.
 * @tpChapter Integration test
 * @tpSince RESTEasy 4.0.0
 */
@ExtendWith(ArquillianExtension.class)
@RunAsClient
public class JsonBindingTest {

    protected static final Logger logger = Logger.getLogger(JsonBindingTest.class.getName());

    static Client client;

    private static final String WAR_WITH_JSONB = "war_with_jsonb";
    private static final String CUSTOM_JSON_PROVIDER = "custom_json_provider";
    private static final String WAR_WITH_JSONB_ASCII_ENCODING = "war_with_jsonb_ascii_encoding";

    @Deployment(name = WAR_WITH_JSONB)
    public static Archive<?> deployWithJsonB() {
        return deploy(WAR_WITH_JSONB, true);
    }

    @Deployment(name = CUSTOM_JSON_PROVIDER)
    public static Archive<?> deployWithoutJsonB() {
        return deploy(CUSTOM_JSON_PROVIDER, false);
    }

    @Deployment(name = WAR_WITH_JSONB_ASCII_ENCODING)
    @TargetsContainer(ContainerConstants.ENCODING_CONTAINER_QUALIFIER)
    public static Archive<?> deploymentWithJsonBAsciiEncoding() {
        return deploy(WAR_WITH_JSONB_ASCII_ENCODING, true);
    }

    public static Archive<?> deploy(String archiveName, boolean useJsonB) {
        WebArchive war = TestUtil.prepareArchive(archiveName);
        war.addClass(JsonBindingTest.class);
        war.addClass(Cat.class);
        if (useJsonB) {
            war.addAsManifestResource("jboss-deployment-structure-json-b.xml", "jboss-deployment-structure.xml");
            return TestUtil.finishContainerPrepare(war, null, JsonBindingResource.class);
        }
        return TestUtil.finishContainerPrepare(war, null, JsonBindingResource.class, JsonBindingCustomRepeaterProvider.class);
    }

    @BeforeAll
    public static void init() {
        client = ClientBuilder.newClient();
    }

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

    /**
     * @tpTestDetails Client sends POST request with a JSON annotated entity. The object should be returned back by the
     *                response and should contain the same field values as original request.
     *
     *                JSON-B is activated on both server and client side
     *                Client should not ignore @JsonbTransient annotation and should not send a value in this variable
     *                Check that server returns object without a value in variable with @JsonbTransient annotation
     *
     * @tpPassCrit The resource returns object with correct values
     * @tpSince RESTEasy 4.0.0
     */
    @Test
    public void jsonbOnServerAndClientTest() throws Exception {
        String charset = "UTF-8";
        WebTarget target = client.target(PortProviderUtil.generateURL("/test/jsonBinding/cat/transient", WAR_WITH_JSONB));
        MediaType mediaType = MediaType.APPLICATION_JSON_TYPE.withCharset(charset);
        Entity<Cat> entity = Entity.entity(
                new Cat("Rosa", "semi-british", "tabby", true, JsonBindingResource.CLIENT_TRANSIENT_VALUE), mediaType);
        Cat json = target.request().post(entity, Cat.class);
        logger.info("Request entity: " + entity);
        Assertions.assertTrue("Alfred".equals(json.getName()), "Failed to return the correct name");
        Assertions.assertEquals(json.getTransientVar(), Cat.DEFAULT_TRANSIENT_VAR_VALUE,
                "Variable with JsonbTransient annotation should be transient, if JSON-B is used");

        String jsonbResponse = target.request().post(entity).readEntity(String.class);
        Assertions.assertEquals("{\"color\":\"ginger\",\"sort\":\"semi-british\",\"name\":\"Alfred\",\"domesticated\":true}",
                jsonbResponse,
                "JsonBindingProvider is not enabled");
    }

    /**
     * @tpTestDetails JSON-B is used on client, JSON-B is not used on server, server uses test's custom json provider
     *                client should not ignore @JsonbTransient annotation and should not send a value in this variable
     *                server verify that client doesn't sent a value in a variable with @JsonbTransient annotation
     *                server returns json data with a value in a variable with @JsonbTransient annotation
     *                client should not ignore @JsonbTransient annotation and should not receive a value in this variable
     *
     * @tpPassCrit The resource returns object with correct values
     * @tpSince RESTEasy 3.5
     */
    @Test
    public void jsonbOnClientTest() throws Exception {
        String charset = "UTF-8";
        WebTarget target = client
                .target(PortProviderUtil.generateURL("/test/jsonBinding/client/test/transient", CUSTOM_JSON_PROVIDER));
        MediaType mediaType = MediaType.APPLICATION_JSON_TYPE.withCharset(charset);
        Entity<Cat> entity = Entity.entity(
                new Cat("Rosa", "semi-british", "tabby", true,
                        JsonBindingResource.CLIENT_TRANSIENT_VALUE),
                mediaType);
        Cat response = target.request().post(entity, Cat.class);
        Assertions.assertEquals(response.getName(), "Rosa",
                "Failed to return the correct name");
        Assertions.assertEquals(response.getTransientVar(), Cat.DEFAULT_TRANSIENT_VAR_VALUE,
                "Variable with JsonbTransient annotation should be transient, if JSON-B is used");
    }

    /**
     * @tpTestDetails JSON-B is used on client, JSON-B is not used on server, server uses test's custom json provider
     *                client should not ignore @JsonbTransient annotation and should not send a value in this variable
     *                server verify that client doesn't sent a value in a variable with @JsonbTransient annotation
     *                server returns json data with a value in a variable with @JsonbTransient annotation
     *                client should not ignore @JsonbTransient annotation and should not receive a value in this variable
     *
     *                This test covers RESTEASY-2171.
     *
     * @tpPassCrit The resource returns object with correct values
     * @tpSince RESTEasy 3.5
     */
    @Test
    @OperateOnDeployment(WAR_WITH_JSONB_ASCII_ENCODING)
    public void jsonbOnClientTestWithoutEncoding() throws Exception {
        URI url = new URI("http://" + PortProviderUtil.getHost() + ":"
                + (PortProviderUtil.getPort() + ContainerConstants.ENCODING_CONTAINER_PORT_OFFSET)
                + "/" + WAR_WITH_JSONB_ASCII_ENCODING + "/test/jsonBinding/repeater");
        WebTarget target = client.target(url);
        MediaType mediaType = MediaType.APPLICATION_JSON_TYPE;
        Entity<Cat> entity = Entity.entity(
                new Cat("Graça", "brazilian", "gray", true,
                        JsonBindingResource.CLIENT_TRANSIENT_VALUE),
                mediaType);
        Cat response = target.request().post(entity, Cat.class);

        Assertions.assertEquals(response.getName(), "Graça", "Failed to return the correct name");
        Assertions.assertEquals(response.getTransientVar(), Cat.DEFAULT_TRANSIENT_VAR_VALUE,
                "Variable with JsonbTransient annotation should be transient, if JSON-B is used");
    }

    /**
     * @tpTestDetails JSON-B is used on client, JSON-B is not used on server, server uses test's custom json provider
     *                client should not ignore @JsonbTransient annotation and should not send a value in this variable
     *                server verify that client doesn't sent a value in a variable with @JsonbTransient annotation
     *                server returns json data with a value in a variable with @JsonbTransient annotation
     *                client should not ignore @JsonbTransient annotation and should not receive a value in this variable
     *
     *                This test covers RESTEASY-2171.
     *
     * @tpPassCrit The resource returns object with correct values
     * @tpSince RESTEasy 3.5
     */
    @Test
    @OperateOnDeployment(WAR_WITH_JSONB_ASCII_ENCODING)
    public void jsonbOnClientTestWithAsciiEncoding() throws Exception {
        URI url = new URI("http://" + PortProviderUtil.getHost() + ":"
                + (PortProviderUtil.getPort() + ContainerConstants.ENCODING_CONTAINER_PORT_OFFSET)
                + "/" + WAR_WITH_JSONB_ASCII_ENCODING + "/test/jsonBinding/repeater");
        WebTarget target = client.target(url);
        MediaType mediaType = MediaType.APPLICATION_JSON_TYPE.withCharset("us-ascii");
        Entity<Cat> entity = Entity.entity(
                new Cat("Graça", "brazilian", "gray", true,
                        JsonBindingResource.CLIENT_TRANSIENT_VALUE),
                mediaType);
        Cat response = target.request().post(entity, Cat.class);

        Assertions.assertEquals(response.getName(), "Graça", "Failed to return the correct name");
        Assertions.assertEquals(response.getTransientVar(), Cat.DEFAULT_TRANSIENT_VAR_VALUE,
                "Variable with JsonbTransient annotation should be transient, if JSON-B is used");
    }

    /**
     * @tpTestDetails JSON-B is used on client, JSON-B is not used on server, server uses test's custom json provider
     *                Client send GET request to server
     *                Server returns Cat object, custom provider uses toString method, that doesn't doesn't create correct JSON
     *                data
     *                Client receive data with "json" media type, but data was created by toString method
     *                JSON-B on client should throw user-friendly exception, because toString method doesn't create correct JSON
     *                data
     * @tpSince RESTEasy 3.5
     */
    @Test
    public void negativeScenarioOnClient() throws Exception {
        // call and log get request
        WebTarget target = client.target(PortProviderUtil.generateURL("/test/jsonBinding/get/cat", CUSTOM_JSON_PROVIDER));
        Response response = target.request().get();
        String responseAsString = response.readEntity(String.class);
        Assertions.assertTrue(responseAsString.contains(Cat.CUSTOM_TO_STRING_FORMAT),
                "Server should use custom JSON provider");
        logger.info("Response as a String: " + responseAsString);
        response.close();

        // call get request, try to get Cat data
        response = target.request().get();
        try {
            Cat wrongObject = response.readEntity(Cat.class);
            logger.info("JSON-B parse server toString method, although JSON-B should not do that. Received object:");
            logger.info(wrongObject.toString());
            Assertions.fail("Client should throw exception because JSON-B should not be able to parse wrong data");
        } catch (Throwable e) {
            StringWriter errors = new StringWriter();
            e.printStackTrace(new PrintWriter(errors));
            String stackTraceString = errors.toString();
            logger.info("StackTrace of exception:");
            logger.info(stackTraceString);
            for (String stackTraceLine : stackTraceString.split(System.lineSeparator())) {
                Assertions.assertFalse(stackTraceLine.contains("Messages (implementation not found)"),
                        "User-unfriendly error message in JSON-B");
            }
        }
    }
}
