package com.fasterxml.jackson.databind.jsontype.ext;

import java.io.IOException;
import java.util.UUID;

import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;

import com.fasterxml.jackson.core.JsonParseException;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.ext.ExternalTypeIdTest1288.ClassesWithBuilder.PaymentMean;
import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;

@SuppressWarnings("hiding")
public class ExternalTypeIdTest1288 extends BaseMapTest
{
    public static class ClassesWithoutBuilder {
        
        public static class CreditCardDetails implements PaymentDetails {
            
            protected String cardHolderFirstName;
            protected String cardHolderLastName;
            protected String number;
            protected String expiryDate;
            protected int csc;
            protected String address;
            protected String zipCode;
            protected String city;
            protected String province;
            
            protected String countryCode;
            
            protected String description;

            public void setCardHolderFirstName (String cardHolderFirstName) {
                this.cardHolderFirstName = cardHolderFirstName;
            }

            public void setCardHolderLastName (String cardHolderLastName) {
                this.cardHolderLastName = cardHolderLastName;
            }

            public void setNumber (String number) {
                this.number = number;
            }

            public void setExpiryDate (String expiryDate) {
                this.expiryDate = expiryDate;
            }

            public void setCsc (int csc) {
                this.csc = csc;
            }

            public void setAddress (String address) {
                this.address = address;
            }

            public void setZipCode (String zipCode) {
                this.zipCode = zipCode;
            }

            public void setCity (String city) {
                this.city = city;
            }

            public void setProvince (String province) {
                this.province = province;
            }

            public void setCountryCode (String countryCode) {
                this.countryCode = countryCode;
            }

            public void setDescription (String description) {
                this.description = description;
            }
            
            
        }
        
        public static class EncryptedCreditCardDetails implements PaymentDetails {
            
            protected UUID paymentInstrumentID;
            
            protected String name;

            public void setPaymentInstrumentID (UUID paymentInstrumentID) {
                this.paymentInstrumentID = paymentInstrumentID;
            }

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

        }
        
        public enum FormOfPayment {
            INDIVIDUAL_CREDIT_CARD (CreditCardDetails.class), COMPANY_CREDIT_CARD (
                    CreditCardDetails.class), INSTRUMENTED_CREDIT_CARD (EncryptedCreditCardDetails.class);
            
            private final Class<? extends PaymentDetails> clazz;
            
            FormOfPayment (final Class<? extends PaymentDetails> clazz) {
                this.clazz = clazz;
            }
            
            @SuppressWarnings ("unchecked")
            public <T extends PaymentDetails> Class<T> getDetailsClass () {
                return (Class<T>) this.clazz;
            }
            
            public static FormOfPayment fromDetailsClass (Class<PaymentDetails> detailsClass) {
                for (FormOfPayment fop : FormOfPayment.values ()) {
                    if (fop.clazz == detailsClass) {
                        return fop;
                    }
                }
                throw new IllegalArgumentException ("not found");
            }
        }
        
        public interface PaymentDetails {
            public interface Builder {
                PaymentDetails build ();
            }
        }
        
        public static class PaymentMean {
            
            FormOfPayment formOfPayment;
            
            PaymentDetails paymentDetails;

            public void setFormOfPayment (FormOfPayment formOfPayment) {
                this.formOfPayment = formOfPayment;
            }

            @JsonTypeInfo (use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "form_of_payment", visible = true)
            @JsonTypeIdResolver (PaymentDetailsTypeIdResolver.class)
            public void setPaymentDetails (PaymentDetails paymentDetails) {
                this.paymentDetails = paymentDetails;
            }
            
        }
        
        public static class PaymentDetailsTypeIdResolver extends TypeIdResolverBase {
            @SuppressWarnings ("unchecked")
            @Override
            public String idFromValue (Object value) {
                if (! (value instanceof PaymentDetails)) {
                    return null;
                }
                return FormOfPayment.fromDetailsClass ((Class<PaymentDetails>) value.getClass ()).name ();
            }
            
            @Override
            public String idFromValueAndType (Object value, Class<?> suggestedType) {
                return this.idFromValue (value);
            }

            @Override
            public JavaType typeFromId (DatabindContext context, String id) {
                return context.getTypeFactory().constructType(FormOfPayment.valueOf(id).getDetailsClass ());
            }

            @Override
            public String getDescForKnownTypeIds () {
                return "PaymentDetails";
            }
            
            @Override
            public Id getMechanism () {
                return JsonTypeInfo.Id.CUSTOM;
            }
        }
    }
    
    public static class ClassesWithBuilder {
        
        @JsonDeserialize (builder = CreditCardDetails.IndividualCreditCardDetailsBuilder.class)
        public static class CreditCardDetails implements PaymentDetails {
            @JsonPOJOBuilder (withPrefix = "")
            public static class CompanyCreditCardDetailsBuilder implements Builder {
                private String cardHolderFirstName;
                private String cardHolderLastName;
                private String number;
                private String expiryDate;
                private int csc;
                private String address;
                private String zipCode;
                private String city;
                private String province;
                private String countryCode;
                
                public CompanyCreditCardDetailsBuilder address (final String a) {
                    address = a;
                    return this;
                }
                
                @Override
                public CreditCardDetails build() {
                    return new CreditCardDetails (this.cardHolderFirstName, this.cardHolderLastName, this.number, this.expiryDate, this.csc, this.address, this.zipCode, this.city,
                            this.province, this.countryCode, "COMPANY CREDIT CARD");
                }
                
                public CompanyCreditCardDetailsBuilder cardHolderFirstName(final String cardHolderFirstName) {
                    this.cardHolderFirstName = cardHolderFirstName;
                    return this;
                }
                
                public CompanyCreditCardDetailsBuilder cardHolderLastName (final String cardHolderLastName) {
                    this.cardHolderLastName = cardHolderLastName;
                    return this;
                }
                
                public CompanyCreditCardDetailsBuilder city (final String city) {
                    this.city = city;
                    return this;
                }
                
                public CompanyCreditCardDetailsBuilder countryCode (final String countryCode) {
                    this.countryCode = countryCode;
                    return this;
                }
                
                public CompanyCreditCardDetailsBuilder csc (final int csc) {
                    this.csc = csc;
                    return this;
                }
                
                public CompanyCreditCardDetailsBuilder expiryDate (final String expiryDate) {
                    this.expiryDate = expiryDate;
                    return this;
                }
                
                public CompanyCreditCardDetailsBuilder number (final String number) {
                    this.number = number;
                    return this;
                }
                
                public CompanyCreditCardDetailsBuilder province (final String province) {
                    this.province = province;
                    return this;
                }
                
                public CompanyCreditCardDetailsBuilder zipCode (final String zipCode) {
                    this.zipCode = zipCode;
                    return this;
                }
            }
            
            @JsonPOJOBuilder (withPrefix = "")
            public static class IndividualCreditCardDetailsBuilder implements Builder {
                private String cardHolderFirstName;
                private String cardHolderLastName;
                private String number;
                private String expiryDate;
                private int    csc;
                private String address;
                private String zipCode;
                private String city;
                private String province;
                private String countryCode;
                private String description;
                
                public IndividualCreditCardDetailsBuilder address (final String address) {
                    this.address = address;
                    return this;
                }
                
                @Override
                public CreditCardDetails build () {
                    return new CreditCardDetails (this.cardHolderFirstName, this.cardHolderLastName, this.number, this.expiryDate, this.csc, this.address, this.zipCode, this.city,
                            this.province, this.countryCode, this.description);
                }
                
                public IndividualCreditCardDetailsBuilder cardHolderFirstName (final String cardHolderFirstName) {
                    this.cardHolderFirstName = cardHolderFirstName;
                    return this;
                }
                
                public IndividualCreditCardDetailsBuilder cardHolderLastName (final String cardHolderLastName) {
                    this.cardHolderLastName = cardHolderLastName;
                    return this;
                }
                
                public IndividualCreditCardDetailsBuilder city (final String city) {
                    this.city = city;
                    return this;
                }
                
                public IndividualCreditCardDetailsBuilder countryCode (final String countryCode) {
                    this.countryCode = countryCode;
                    return this;
                }
                
                public IndividualCreditCardDetailsBuilder csc (final int csc) {
                    this.csc = csc;
                    return this;
                }
                
                public IndividualCreditCardDetailsBuilder description (final String description) {
                    this.description = description;
                    return this;
                }
                
                public IndividualCreditCardDetailsBuilder expiryDate (final String expiryDate) {
                    this.expiryDate = expiryDate;
                    return this;
                }
                
                public IndividualCreditCardDetailsBuilder number (final String number) {
                    this.number = number;
                    return this;
                }
                
                public IndividualCreditCardDetailsBuilder province (final String p) {
                    province = p;
                    return this;
                }
                
                public IndividualCreditCardDetailsBuilder zipCode (final String z) {
                    zipCode = z;
                    return this;
                }
                
            }
            
            protected final String cardHolderFirstName;
            protected final String cardHolderLastName;
            protected final String number;
            protected final String expiryDate;
            protected final int    csc;
            protected final String address;
            protected final String zipCode;
            protected final String city;
            protected final String province;
            
            protected final String countryCode;
            
            protected final String description;
            
            public CreditCardDetails (final String cardHolderFirstName, final String cardHolderLastName, final String number, final String expiryDate, final int csc,
                    final String address, final String zipCode, final String city, final String province, final String countryCode, final String description) {
                super ();
                this.cardHolderFirstName = cardHolderFirstName;
                this.cardHolderLastName = cardHolderLastName;
                this.number = number;
                this.expiryDate = expiryDate;
                this.csc = csc;
                this.address = address;
                this.zipCode = zipCode;
                this.city = city;
                this.province = province;
                this.countryCode = countryCode;
                this.description = description;
            }
        }
        
        @JsonDeserialize (builder = EncryptedCreditCardDetails.InstrumentedCreditCardBuilder.class)
        public static class EncryptedCreditCardDetails implements PaymentDetails {
            @JsonPOJOBuilder (withPrefix = "")
            public static class InstrumentedCreditCardBuilder implements Builder {
                private UUID   paymentInstrumentID;
                private String name;
                
                @Override
                public EncryptedCreditCardDetails build () {
                    return new EncryptedCreditCardDetails (this.paymentInstrumentID, this.name);
                }
                
                public InstrumentedCreditCardBuilder name (final String name) {
                    this.name = name;
                    return this;
                }
                
                public InstrumentedCreditCardBuilder paymentInstrumentID (final UUID paymentInstrumentID) {
                    this.paymentInstrumentID = paymentInstrumentID;
                    return this;
                }
            }

            protected final UUID paymentInstrumentID;
            protected final String name;

            private EncryptedCreditCardDetails (final UUID paymentInstrumentID, final String name) {
                super ();
                this.paymentInstrumentID = paymentInstrumentID;
                this.name = name;
            }
        }
        
        public enum FormOfPayment {
            INDIVIDUAL_CREDIT_CARD (CreditCardDetails.IndividualCreditCardDetailsBuilder.class), COMPANY_CREDIT_CARD (
                    CreditCardDetails.CompanyCreditCardDetailsBuilder.class), INSTRUMENTED_CREDIT_CARD (EncryptedCreditCardDetails.InstrumentedCreditCardBuilder.class);
            
            private final Class<? extends PaymentDetails.Builder> builderClass;
            
            FormOfPayment (final Class<? extends PaymentDetails.Builder> builderClass) {
                this.builderClass = builderClass;
            }
            
            @SuppressWarnings ("unchecked")
            public <T extends PaymentDetails> Class<T> getDetailsClass () {
                return (Class<T>) this.builderClass.getEnclosingClass ();
            }
            
            public static FormOfPayment fromDetailsClass (Class<PaymentDetails> detailsClass) {
                for (FormOfPayment fop : FormOfPayment.values ()) {
                    if (fop.builderClass.getEnclosingClass () == detailsClass) {
                        return fop;
                    }
                }
                throw new IllegalArgumentException ("not found");
            }
        }
        
        public interface PaymentDetails {
            public interface Builder {
                PaymentDetails build ();
            }
        }
        
        @JsonDeserialize (builder = PaymentMean.Builder.class)
        public static class PaymentMean {
            
            @JsonPOJOBuilder (withPrefix = "")
            @JsonPropertyOrder ({ "form_of_payment", "payment_details" })
            public static class Builder {
                private FormOfPayment  formOfPayment;
                private PaymentDetails paymentDetails;
                
                public PaymentMean build () {
                    return new PaymentMean (this.formOfPayment, this.paymentDetails);
                }
                
                // if you annotate with @JsonIgnore, it works, but the value
                // disappears in the constructor
                public Builder formOfPayment (final FormOfPayment val) {
                    this.formOfPayment = val;
                    return this;
                }

                @JsonTypeInfo (use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "form_of_payment", visible = true)
                @JsonTypeIdResolver (PaymentDetailsTypeIdResolver.class)
                public Builder paymentDetails (final PaymentDetails val) {
                    this.paymentDetails = val;
                    return this;
                }
            }
            
            public static Builder create() {
                return new Builder();
            }

            protected final FormOfPayment  formOfPayment;
            protected final PaymentDetails paymentDetails;

            private PaymentMean (final FormOfPayment formOfPayment, final PaymentDetails paymentDetails) {
                super ();
                this.formOfPayment = formOfPayment;
                this.paymentDetails = paymentDetails;
            }
        }
        
        public static class PaymentDetailsTypeIdResolver extends TypeIdResolverBase {
            @SuppressWarnings ("unchecked")
            @Override
            public String idFromValue (Object value) {
                if (! (value instanceof PaymentDetails)) {
                    return null;
                }
                return FormOfPayment.fromDetailsClass ((Class<PaymentDetails>) value.getClass ()).name ();
            }
            
            @Override
            public String idFromValueAndType (Object value, Class<?> suggestedType) {
                return this.idFromValue (value);
            }

            @Override
            public JavaType typeFromId (DatabindContext context, String id) {
                return context.getTypeFactory().constructType(FormOfPayment.valueOf (id).getDetailsClass ());
            }
            
            @Override
            public String getDescForKnownTypeIds () {
                return "PaymentDetails";
            }
            
            @Override
            public Id getMechanism () {
                return JsonTypeInfo.Id.CUSTOM;
            }
        }
    }

    public void testVisibleExternalTypeId1288() throws Exception
    {
        // given
        final String asJson1 = "{\"form_of_payment\":\"INDIVIDUAL_CREDIT_CARD\", \"payment_details\":{\"card_holder_first_name\":\"John\", \"card_holder_last_name\":\"Doe\",  \"number\":\"XXXXXXXXXXXXXXXX\", \"expiry_date\":\"MM/YY\","
                + "\"csc\":666,\"address\":\"10 boulevard de Sebastopol\",\"zip_code\":\"75001\",\"city\":\"Paris\",\"province\":\"Ile-de-France\",\"country_code\":\"FR\",\"description\":\"John Doe personal credit card\"}}";
        final String asJson2 = "{\"form_of_payment\":\"INSTRUMENTED_CREDIT_CARD\",\"payment_details\":{\"payment_instrument_id\":\"00000000-0000-0000-0000-000000000000\", \"name\":\"Mr John Doe encrypted credit card\"}}";
        final ObjectMapper objectMapper = new ObjectMapper ().setPropertyNamingStrategy (PropertyNamingStrategy.SNAKE_CASE);
//                .disable (DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        ClassesWithoutBuilder.PaymentMean ob1 = objectMapper.readValue (asJson1, ClassesWithoutBuilder.PaymentMean.class);
        assertNotNull(ob1);
        ClassesWithBuilder.PaymentMean ob2 = objectMapper.readValue (asJson2, ClassesWithBuilder.PaymentMean.class);
        assertNotNull(ob2);

    }
}
