/*
 * 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.mapper;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import jakarta.json.JsonValue;

import java.beans.ConstructorProperties;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;

@RunWith(value = Parameterized.class)
public class ObjectConverterWithAnnotationTest {

    private static final String MANUFACTURER_ID = "manufacturerId";
    private static final String TYPE_INDEX = "typeIndex";


    @Parameterized.Parameter
    public String accessMode;


    @Parameterized.Parameters()
    public static Iterable<String> accessModes() {
        return Arrays.asList("field", "method", "both", "strict-method");
    }


    @Test
    public void testSerializeWithObjectConverter() {

        Mapper mapper = new MapperBuilder().setAccessModeName(accessMode)
                                           .setAttributeOrder(String.CASE_INSENSITIVE_ORDER)
                                           .build();

        Cyclist cyclist = new Cyclist("Peter Sagan", new Bike("Specialized / S-Works", BikeType.ROAD));

        String json = mapper.writeObjectAsString(cyclist);
        Assert.assertNotNull(json);
        Assert.assertEquals("{" +
                              "\"bike\":{" +
                                "\"" + MANUFACTURER_ID + "\":0," +
                                "\"" + TYPE_INDEX + "\":0" +
                              "}," +
                              "\"name\":\"Peter Sagan\"" +
                            "}", json);

    }

    @Test
    public void testDeserializeWithObjectConverter() {

        Mapper mapper = new MapperBuilder().setAccessModeName(accessMode)
                                           .build();

        String json = "{" +
                        "\"name\":\"Jan Frodeno\"," +
                        "\"bike\":{" +
                          "\"" + MANUFACTURER_ID + "\":1," +
                          "\"" + TYPE_INDEX + "\":2" +
                        "}" +
                      "}";

        Cyclist expected = new Cyclist("Jan Frodeno", new Bike("Canyon", BikeType.TRIATHLON));

        Object cyclist = mapper.readObject(json, Cyclist.class);
        Assert.assertNotNull(cyclist);
        Assert.assertEquals(expected, cyclist);
    }


    @Test
    public void testSerializeObjectWithCollectionAndObjectConverter() {

        Mapper mapper = new MapperBuilder().setAccessModeName(accessMode)
                                           .setAttributeOrder(String.CASE_INSENSITIVE_ORDER)
                                           .build();

        CycleRace tourDeFrance = new CycleRace(false, true,
                                               Arrays.asList(new Cyclist("Christopher Froome", new Bike("Specialized / S-Works", BikeType.ROAD)),
                                                             new Cyclist("Alejandro Valverde", new Bike("Canyon", BikeType.ROAD)),
                                                             new Cyclist("Andr\u00e9 Greipel", new Bike("Trek", BikeType.ROAD))));// i know they don't have Trek bikes ;)

        String json = mapper.writeObjectAsString(tourDeFrance);
        Assert.assertNotNull(json);
        Assert.assertEquals("{" +
                              "\"cyclists\":[" +
                                "{" +
                                  "\"bike\":{" +
                                    "\"" + MANUFACTURER_ID + "\":0," +
                                    "\"" + TYPE_INDEX + "\":0" +
                                  "}," +
                                  "\"name\":\"Christopher Froome\"" +
                                "}," +
                                "{" +
                                  "\"bike\":{" +
                                    "\"" + MANUFACTURER_ID + "\":1," +
                                    "\"" + TYPE_INDEX + "\":0" +
                                  "}," +
                                  "\"name\":\"Alejandro Valverde\"" +
                                "}," +
                                "{" +
                                  "\"bike\":{" +
                                    "\"" + MANUFACTURER_ID + "\":2," +
                                    "\"" + TYPE_INDEX + "\":0" +
                                  "}," +
                                  "\"name\":\"Andr\u00e9 Greipel\"" +
                                "}" +
                              "]," +
                              "\"monument\":false," +
                              "\"tour\":true" +
                            "}", json);
    }

    @Test
    public void testDeserializeObjectWithCollectionAndObjectConverter() {

        CycleRace expected = new CycleRace(true, false,
                                           Arrays.asList(new Cyclist("Alexander Kristoff", new Bike("Canyon", BikeType.ROAD))));

        Mapper mapper = new MapperBuilder().setAccessModeName(accessMode)
                                           .build();

        String json = "{" +
                        "\"cyclists\":[" +
                          "{" +
                            "\"bike\": {" +
                              "\"" + MANUFACTURER_ID + "\":1," +
                              "\"" + TYPE_INDEX +"\":0" +
                            "}," +
                            "\"name\":\"Alexander Kristoff\"" +
                          "}" +
                        "]," +
                        "\"monument\":true," +
                        "\"tour\":false" +
                      "}";

        Object tourDeFlanderen = mapper.readObject(json, CycleRace.class);
        Assert.assertNotNull(tourDeFlanderen);
        Assert.assertEquals(expected, tourDeFlanderen);
    }

    @Test
    public void testDeserializeObjectWithAnnotatedConsturctorParameter() {

        String json = "{" +
                        "\"bike\": {" +
                          "\"" + MANUFACTURER_ID + "\":1," +
                          "\"" + TYPE_INDEX + "\":0" +
                        "}" +
                      "}";

        Mapper mapper = new MapperBuilder().setAccessModeName(accessMode)
                                           .setReadAttributeBeforeWrite(true)
                                           .build();

        BikeWrapper bikeWrapper = mapper.readObject(json, BikeWrapper.class);
        Assert.assertNotNull(bikeWrapper);
        Assert.assertEquals(bikeWrapper.getBike(), new Bike("Canyon", BikeType.ROAD));
    }

    public static class BikeWrapper {
        private final Bike bike;

        @ConstructorProperties(value = "bike")
        BikeWrapper(@JohnzonConverter(value = BikeConverter.class) Bike bike) {
            this.bike = bike;
        }

        Bike getBike() {
            return bike;
        }
    }


    public static class CycleRace {
        private boolean monument;
        private boolean tour;

        private List<Cyclist> cyclists;

        public CycleRace() {
        }

        public CycleRace(boolean monument, boolean tour, List<Cyclist> cyclists) {
            this.monument = monument;
            this.tour = tour;
            this.cyclists = cyclists;
        }

        public boolean isMonument() {
            return monument;
        }

        public void setMonument(boolean monument) {
            this.monument = monument;
        }

        public boolean isTour() {
            return tour;
        }

        public void setTour(boolean tour) {
            this.tour = tour;
        }

        public List<Cyclist> getCyclists() {
            return cyclists;
        }

        public void setCyclists(List<Cyclist> cyclists) {
            this.cyclists = cyclists;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            CycleRace cycleRace = (CycleRace) o;

            if (monument != cycleRace.monument) {
                return false;
            }
            if (tour != cycleRace.tour) {
                return false;
            }
            return cyclists != null ? cyclists.equals(cycleRace.cyclists) : cycleRace.cyclists == null;

        }

        @Override
        public int hashCode() {
            int result = (monument ? 1 : 0);
            result = 31 * result + (tour ? 1 : 0);
            result = 31 * result + (cyclists != null ? cyclists.hashCode() : 0);
            return result;
        }
    }

    public static class Cyclist {

        private String name;

        @JohnzonConverter(value = BikeConverter.class)
        private Bike bike;

        public Cyclist() {
        }

        public Cyclist(String name, Bike bike) {
            this.name = name;
            this.bike = bike;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @JohnzonConverter(value = BikeConverter.class)
        public Bike getBike() {
            return bike;
        }

        @JohnzonConverter(value = BikeConverter.class)
        public void setBike(Bike bike) {
            this.bike = bike;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            Cyclist cyclist = (Cyclist) o;

            if (name != null ? !name.equals(cyclist.name) : cyclist.name != null) {
                return false;
            }
            return bike != null ? bike.equals(cyclist.bike) : cyclist.bike == null;

        }

        @Override
        public int hashCode() {
            int result = name != null ? name.hashCode() : 0;
            result = 31 * result + (bike != null ? bike.hashCode() : 0);
            return result;
        }
    }

    public static class Bike {
        private String manufacturer;
        private BikeType type;

        public Bike() {
        }

        public Bike(String manufacturer, BikeType type) {
            this.manufacturer = manufacturer;
            this.type = type;
        }

        public String getManufacturer() {
            return manufacturer;
        }

        public void setManufacturer(String manufacturer) {
            this.manufacturer = manufacturer;
        }

        public BikeType getType() {
            return type;
        }

        public void setType(BikeType type) {
            this.type = type;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            Bike bike = (Bike) o;

            if (manufacturer != null ? !manufacturer.equals(bike.manufacturer) : bike.manufacturer != null) {
                return false;
            }
            return type == bike.type;

        }

        @Override
        public int hashCode() {
            int result = manufacturer != null ? manufacturer.hashCode() : 0;
            result = 31 * result + (type != null ? type.hashCode() : 0);
            return result;
        }
    }

    public enum BikeType {
        ROAD,
        TIME_TRIAL,
        TRIATHLON
    }

    public static class BikeConverter implements ObjectConverter.Codec<Bike> {

        public static final List<String> MANUFACTURERS = Arrays.asList("Specialized / S-Works", "Canyon", "Trek", "Scott");


        @Override
        public void writeJson(Bike instance, MappingGenerator jsonbGenerator) {
            jsonbGenerator.getJsonGenerator().write(MANUFACTURER_ID, MANUFACTURERS.indexOf(instance.getManufacturer()));

            // i know you should never use this in production but its good for our sample ;)
            jsonbGenerator.getJsonGenerator().write(TYPE_INDEX, instance.getType().ordinal());
        }

        @Override
        public Bike fromJson(JsonValue jsonObject, Type targetType, MappingParser parser) {
            return new Bike(MANUFACTURERS.get(jsonObject.asJsonObject().getInt(MANUFACTURER_ID)),
                            BikeType.values()[jsonObject.asJsonObject().getInt(TYPE_INDEX)]);
        }
    }
}
