/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.camel.quarkus.component.jaxb.it;

import java.io.StringWriter;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import org.apache.camel.quarkus.component.jaxb.it.model.Person;
import org.apache.camel.quarkus.component.jaxb.it.model.namespaced.NamespacedPerson;
import org.apache.camel.quarkus.component.jaxb.it.model.simple.SimplePerson;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static io.restassured.config.XmlConfig.xmlConfig;
import static org.apache.camel.quarkus.component.jaxb.it.model.namespaced.NamespacedPerson.NAMESPACE;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.is;

@QuarkusTest
class JaxbTest {

    private static final String PERSON_FIRST_NAME = "John";
    private static final String PERSON_LAST_NAME = "Doe";
    private static final int PERSON_AGE = 25;

    @Test
    public void marshall() {
        RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .get("/jaxb/marshal")
                .then()
                .statusCode(200)
                .body(
                        "person.firstName", is(PERSON_FIRST_NAME),
                        "person.lastName", is(PERSON_LAST_NAME),
                        "person.age", is(String.valueOf(PERSON_AGE)));
    }

    @Test
    public void unmarshall() {
        Person person = new SimplePerson();
        person.setFirstName(PERSON_FIRST_NAME);
        person.setLastName(PERSON_LAST_NAME);
        person.setAge(PERSON_AGE);

        String xml = getPersonXml(person);
        RestAssured.given()
                .contentType(ContentType.XML)
                .body(xml)
                .post("/jaxb/unmarshal")
                .then()
                .statusCode(200)
                .body(
                        "firstName", is(PERSON_FIRST_NAME),
                        "lastName", is(PERSON_LAST_NAME),
                        "age", is(PERSON_AGE));
    }

    @Test
    public void marshallWithoutMandatoryField() {
        RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("age", PERSON_AGE)
                .get("/jaxb/marshal")
                .then()
                .statusCode(500)
                .body("error", containsString("'{lastName}'"));
    }

    @Test
    public void unmarshallWithoutMandatoryField() {
        Person person = new SimplePerson();
        person.setFirstName(PERSON_FIRST_NAME);
        person.setAge(PERSON_AGE);

        String xml = getPersonXml(person);
        RestAssured.given()
                .contentType(ContentType.XML)
                .body(xml)
                .post("/jaxb/unmarshal")
                .then()
                .statusCode(500)
                .body("error", containsString("'{lastName}'"));
    }

    @Test
    public void marshallWithJaxbDsl() {
        RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .get("/jaxb/marshal/dsl")
                .then()
                .statusCode(200)
                .body(
                        "person.firstName", is(PERSON_FIRST_NAME),
                        "person.lastName", is(PERSON_LAST_NAME),
                        "person.age", is(String.valueOf(PERSON_AGE)));
    }

    @Test
    public void unmarshallWithJaxbDsl() {
        Person person = new SimplePerson();
        person.setFirstName(PERSON_FIRST_NAME);
        person.setLastName(PERSON_LAST_NAME);
        person.setAge(PERSON_AGE);

        String xml = getPersonXml(person);
        RestAssured.given()
                .contentType(ContentType.XML)
                .body(xml)
                .post("/jaxb/unmarshal/dsl")
                .then()
                .statusCode(200)
                .body(
                        "firstName", is(PERSON_FIRST_NAME),
                        "lastName", is(PERSON_LAST_NAME),
                        "age", is(PERSON_AGE));
    }

    @Test
    public void marshallWithNamespacePrefix() {
        RestAssured.given()
                .config(RestAssured.config().xmlConfig(xmlConfig().declareNamespace("test", NAMESPACE)))
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .get("/jaxb/marshal/namespace/prefix")
                .then()
                .statusCode(200)
                .body(
                        "person.firstName", is(PERSON_FIRST_NAME),
                        "person.lastName", is(PERSON_LAST_NAME),
                        "person.age", is(String.valueOf(PERSON_AGE)));
    }

    @Test
    public void unmarshallWithNamespacePrefix() {
        Person person = new NamespacedPerson();
        person.setFirstName(PERSON_FIRST_NAME);
        person.setLastName(PERSON_LAST_NAME);
        person.setAge(PERSON_AGE);

        String xml = getPersonXml(person);
        RestAssured.given()
                .contentType(ContentType.XML)
                .body(xml)
                .post("/jaxb/unmarshal/namespace/prefix")
                .then()
                .statusCode(200)
                .body(
                        "firstName", is(PERSON_FIRST_NAME),
                        "lastName", is(PERSON_LAST_NAME),
                        "age", is(PERSON_AGE));
    }

    @Test
    public void marshallWithEncoding() {
        String firstName = PERSON_FIRST_NAME.replace("o", "ø");
        // Add some invalid characters that the filterNonXmlChars option will replace with spaces
        String lastName = PERSON_LAST_NAME + " \uD83D\uDC33";
        RestAssured.given()
                .contentType(ContentType.TEXT)
                .queryParam("firstName", firstName)
                .queryParam("lastName", lastName)
                .queryParam("age", PERSON_AGE)
                .get("/jaxb/marshal/encoding")
                .then()
                .statusCode(200)
                .body(
                        "person.firstName", endsWith(firstName),
                        "person.lastName", is(PERSON_LAST_NAME + "   "),
                        "person.age", is(String.valueOf(PERSON_AGE)));
    }

    @Test
    public void unmarshallWithEncoding() {
        String firstName = PERSON_FIRST_NAME.replace("o", "ø");
        Person person = new SimplePerson();
        person.setFirstName(firstName);
        person.setLastName(PERSON_LAST_NAME);
        person.setAge(PERSON_AGE);

        String xml = getPersonXml(person, "ISO-8859-1");
        RestAssured.given()
                .contentType(ContentType.XML)
                .body(xml)
                .post("/jaxb/unmarshal/encoding")
                .then()
                .statusCode(200)
                .body(
                        "firstName", is(firstName),
                        "lastName", is(PERSON_LAST_NAME),
                        "age", is(PERSON_AGE));
    }

    @Test
    public void marshalWithExistingXmlPayload() {
        Person person = new SimplePerson();
        person.setFirstName(PERSON_FIRST_NAME);
        person.setLastName(PERSON_LAST_NAME);
        person.setAge(PERSON_AGE);

        String xml = getPersonXml(person);
        RestAssured.given()
                .contentType(ContentType.XML)
                .body(xml)
                .post("/jaxb/marshal/xml")
                .then()
                .statusCode(200)
                .body(
                        "person.firstName", is(PERSON_FIRST_NAME),
                        "person.lastName", is(PERSON_LAST_NAME),
                        "person.age", is(String.valueOf(PERSON_AGE)));
    }

    @ParameterizedTest
    @ValueSource(booleans = { false, true })
    public void marshalWithPartClass(boolean useHeader) {
        RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .queryParam("useHeader", useHeader)
                .get("/jaxb/marshal/part/class")
                .then()
                .statusCode(200)
                .body(
                        "person.firstName", is(PERSON_FIRST_NAME),
                        "person.lastName", is(PERSON_LAST_NAME),
                        "person.age", is(String.valueOf(PERSON_AGE)));
    }

    @ParameterizedTest
    @ValueSource(booleans = { false, true })
    public void unmarshalWithPartClass(boolean useHeader) {
        String personXml = RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .queryParam("useHeader", useHeader)
                .get("/jaxb/marshal/part/class")
                .then()
                .statusCode(200)
                .extract()
                .body()
                .asString();

        RestAssured.given()
                .queryParam("useHeader", useHeader)
                .contentType(ContentType.XML)
                .body(personXml)
                .post("/jaxb/unmarshal/part/class")
                .then()
                .statusCode(200)
                .body(
                        "firstName", is(PERSON_FIRST_NAME),
                        "lastName", is(PERSON_LAST_NAME),
                        "age", is(PERSON_AGE));
    }

    @Test
    public void unmarshalWithIgnoreJaxbElement() {
        String personXml = RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .queryParam("useHeader", false)
                .get("/jaxb/marshal/part/class")
                .then()
                .statusCode(200)
                .extract()
                .body()
                .asString();

        RestAssured.given()
                .contentType(ContentType.XML)
                .body(personXml)
                .post("/jaxb/unmarshal/ignore/element")
                .then()
                .statusCode(200)
                .body(
                        "firstName", is(PERSON_FIRST_NAME),
                        "lastName", is(PERSON_LAST_NAME),
                        "age", is(PERSON_AGE));
    }

    @Test
    public void marshallWithCustomProperties() {
        String[] lines = RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .get("/jaxb/marshal/custom/properties")
                .then()
                .statusCode(200)
                .extract()
                .body()
                .asString()
                .split(System.lineSeparator());

        // Pretty printing is disabled so we should have only one line
        Assertions.assertEquals(1, lines.length);
    }

    @Test
    public void marshalWithCustomStreamWriter() {
        RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .get("/jaxb/marshal/custom/stream/writer")
                .then()
                .statusCode(200)
                .body(
                        "person-modified.firstName-modified", is(PERSON_FIRST_NAME),
                        "person-modified.lastName-modified", is(PERSON_LAST_NAME),
                        "person-modified.age-modified", is(String.valueOf(PERSON_AGE)));
    }

    @Test
    public void marshalWithObjectFactory() {
        RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .get("/jaxb/marshal/with/object/factory")
                .then()
                .log().body()
                .statusCode(200)
                .body(
                        "person.firstName", is(PERSON_FIRST_NAME),
                        "person.lastName", is(PERSON_LAST_NAME),
                        "person.age", is(String.valueOf(PERSON_AGE)));
    }

    @Test
    public void marshalWithoutObjectFactory() {
        RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .get("/jaxb/marshal/without/object/factory")
                .then()
                .statusCode(200)
                .body(
                        "firstName", is(PERSON_FIRST_NAME),
                        "lastName", is(PERSON_LAST_NAME),
                        "age", is(PERSON_AGE));
    }

    @Test
    public void marshalWithNoNamespaceSchemaLocation() {
        RestAssured.given()
                .queryParam("firstName", PERSON_FIRST_NAME)
                .queryParam("lastName", PERSON_LAST_NAME)
                .queryParam("age", PERSON_AGE)
                .get("/jaxb/marshal/non/namespace/schema/location")
                .then()
                .statusCode(200)
                .body(containsString("noNamespaceSchemaLocation=\"person-no-namespace.xsd\""));
    }

    private static String getPersonXml(Person person) {
        return getPersonXml(person, "UTF-8");
    }

    private static String getPersonXml(Person person, String encoding) {
        try {
            StringWriter writer = new StringWriter();
            JAXBContext context = JAXBContext.newInstance(person.getClass());
            Marshaller marshaller = context.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_ENCODING, encoding);
            marshaller.marshal(person, writer);
            return writer.toString();
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }
}
