/*
 * 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.johnzon.jsonb.polymorphism;

import jakarta.json.bind.annotation.JsonbCreator;
import jakarta.json.bind.annotation.JsonbDateFormat;
import jakarta.json.bind.annotation.JsonbProperty;
import org.apache.johnzon.jsonb.test.JsonbRule;
import org.hamcrest.CoreMatchers;
import org.junit.Rule;
import org.junit.Test;

import jakarta.json.bind.annotation.JsonbSubtype;
import jakarta.json.bind.annotation.JsonbTypeInfo;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class JsonbPolymorphismTest {

    @Rule public JsonbRule jsonb = new JsonbRule();

    @Test
    public void testNestedSerialization() {
        Labrador labrador = new Labrador();
        labrador.dogAge = 3;
        labrador.labradorName = "john";
        AnimalWrapper wrapper = new AnimalWrapper();
        wrapper.animal = labrador;

        assertEquals("{\"animal\":{\"@animal\":\"dog\",\"@dog\":\"labrador\",\"dogAge\":3,\"labradorName\":\"john\"}}", jsonb.toJson(wrapper));
    }

    @Test
    public void testSerialization() {
        Labrador labrador = new Labrador();
        labrador.dogAge = 3;
        labrador.labradorName = "john";

        assertEquals("{\"@animal\":\"dog\",\"@dog\":\"labrador\",\"dogAge\":3,\"labradorName\":\"john\"}",
                jsonb.toJson(labrador));
    }

    @Test
    public void testDeserialization() {
        Animal deserialized = jsonb.fromJson("{\"@animal\":\"dog\",\"@dog\":\"labrador\",\"dogAge\":3,\"labradorName\":\"john\"}", Animal.class);
        assertTrue(deserialized instanceof Labrador);
        assertEquals(3, ((Labrador) deserialized).dogAge);
        assertEquals("john", ((Labrador) deserialized).labradorName);
    }

    @Test
    public void testSubtypeSelfSerialization() {
        Dog dog = new Dog();
        dog.dogAge = 3;

        assertEquals("{\"@animal\":\"dog\",\"@dog\":\"other\",\"dogAge\":3}",
                jsonb.toJson(dog));
    }

    @Test
    public void testSubtypeSelfDeserialization() {
        Animal deserialized = jsonb.fromJson("{\"@animal\":\"dog\",\"@dog\":\"other\",\"dogAge\":3}", Animal.class);

        assertTrue(deserialized instanceof Dog);
        assertEquals(3, ((Dog) deserialized).dogAge);
    }

    @Test
    public void testNoTypeInformationInJson() {
        Dog dog = jsonb.fromJson("{\"dogAge\":3}", Dog.class);

        assertEquals(3, dog.dogAge);
    }

    @Test
    public void testCreatorDeserialization() {
        SomeDateType deserialized = jsonb.fromJson("{\"@dateType\":\"constructor\",\"localDate\":\"26-02-2021\"}",
                                                   SomeDateType.class);

        assertThat("Incorrectly deserialized according to the type information. Expected was DateConstructor instance. "
                   + "Got instance of class " + deserialized.getClass(),
                   deserialized, CoreMatchers.instanceOf(DateConstructor.class));

        final DateTimeFormatter formatter =
            DateTimeFormatter.ofPattern("dd-MM-yyyy").withLocale(Locale.forLanguageTag("nl-NL"));
        assertThat("Date isn't the expected one",
                   ((DateConstructor) deserialized).getLocalDate(),
                   CoreMatchers.equalTo(LocalDate.parse("26-02-2021", formatter)));

    }

    public static class AnimalWrapper {
        public Animal animal;
    }

    @JsonbTypeInfo(key = "@animal", value = @JsonbSubtype(alias = "dog", type = Dog.class))
    public interface Animal {
    }

    @JsonbTypeInfo(key = "@dog", value = {
            @JsonbSubtype(alias = "other", type = Dog.class),
            @JsonbSubtype(alias = "labrador", type = Labrador.class)
    })
    public static class Dog implements Animal {
        public int dogAge;
    }

    public static class Labrador extends Dog {
        public String labradorName;
    }

    @JsonbTypeInfo(key = "@dateType", value = {
        @JsonbSubtype(alias = "constructor", type = DateConstructor.class)
    })
    public interface SomeDateType {

    }

    public static final class DateConstructor implements SomeDateType {

        private LocalDate localDate;

        @JsonbCreator
        public DateConstructor(@JsonbProperty("localDate") @JsonbDateFormat(value = "dd-MM-yyyy", locale = "nl-NL") LocalDate localDate) {
            this.localDate = localDate;
        }

        public LocalDate getLocalDate() {
            return localDate;
        }
    }

    @Test
    public void typeInfoNotOnDirectParent() {
        ConcreteSomething something = new ConcreteSomething();
        assertEquals("{\"@type\":\"concrete\"}", jsonb.toJson(something));
    }

    @JsonbTypeInfo(
            @JsonbSubtype(alias = "concrete", type = ConcreteSomething.class)
    )
    public static abstract class AbstractTopLevelSomething { }
    public static abstract class AbstractMiddleLevelSomething extends AbstractTopLevelSomething { }
    public static class ConcreteSomething extends AbstractMiddleLevelSomething { }
}
