// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.number;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
// TODO: enable in Java 8: import java.lang.reflect.Parameter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.junit.Test;

import com.ibm.icu.dev.test.serializable.SerializableTestUtility;
import com.ibm.icu.impl.number.DecimalFormatProperties;
import com.ibm.icu.impl.number.DecimalFormatProperties.ParseMode;
import com.ibm.icu.impl.number.Padder.PadPosition;
import com.ibm.icu.impl.number.PatternStringParser;
import com.ibm.icu.text.CompactDecimalFormat.CompactStyle;
import com.ibm.icu.text.CurrencyPluralInfo;
import com.ibm.icu.text.MeasureFormat.FormatWidth;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.util.Currency;
import com.ibm.icu.util.Currency.CurrencyUsage;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;

public class PropertiesTest {

    @Test
    public void testBasicEquals() {
        DecimalFormatProperties p1 = new DecimalFormatProperties();
        DecimalFormatProperties p2 = new DecimalFormatProperties();
        assertEquals(p1, p2);

        p1.setPositivePrefix("abc");
        assertNotEquals(p1, p2);
        p2.setPositivePrefix("xyz");
        assertNotEquals(p1, p2);
        p1.setPositivePrefix("xyz");
        assertEquals(p1, p2);
    }

    @Test
    public void testFieldCoverage() {
        DecimalFormatProperties p0 = new DecimalFormatProperties();
        DecimalFormatProperties p1 = new DecimalFormatProperties();
        DecimalFormatProperties p2 = new DecimalFormatProperties();
        DecimalFormatProperties p3 = new DecimalFormatProperties();
        DecimalFormatProperties p4 = new DecimalFormatProperties();

        Set<Integer> hashCodes = new HashSet<Integer>();
        Field[] fields = DecimalFormatProperties.class.getDeclaredFields();
        for (Field field : fields) {
            if (Modifier.isStatic(field.getModifiers())) {
                continue;
            }

            // Check for getters and setters
            String fieldNamePascalCase = Character.toUpperCase(field.getName().charAt(0))
                    + field.getName().substring(1);
            String getterName = "get" + fieldNamePascalCase;
            String setterName = "set" + fieldNamePascalCase;
            Method getter, setter;
            try {
                getter = DecimalFormatProperties.class.getMethod(getterName);
                assertEquals("Getter does not return correct type",
                        field.getType(),
                        getter.getReturnType());
            } catch (NoSuchMethodException e) {
                fail("Could not find method " + getterName + " for field " + field);
                continue;
            } catch (SecurityException e) {
                fail("Could not access method " + getterName + " for field " + field);
                continue;
            }
            try {
                setter = DecimalFormatProperties.class.getMethod(setterName, field.getType());
                assertEquals("Method " + setterName + " does not return correct type",
                        DecimalFormatProperties.class,
                        setter.getReturnType());
            } catch (NoSuchMethodException e) {
                fail("Could not find method " + setterName + " for field " + field);
                continue;
            } catch (SecurityException e) {
                fail("Could not access method " + setterName + " for field " + field);
                continue;
            }

            // Check for parameter name equality.
            // The parameter name is not always available, depending on compiler settings.
            // TODO: Enable in Java 8
            /*
             * Parameter param = setter.getParameters()[0]; if (!param.getName().subSequence(0,
             * 3).equals("arg")) { assertEquals("Parameter name should equal field name",
             * field.getName(), param.getName()); }
             */

            try {
                // Check for default value (should be null for objects)
                if (field.getType() != Integer.TYPE && field.getType() != Boolean.TYPE) {
                    Object default0 = getter.invoke(p0);
                    assertEquals("Field " + field + " has non-null default value:", null, default0);
                }

                // Check for getter, equals, and hash code behavior
                Object val0 = getSampleValueForType(field.getType(), 0);
                Object val1 = getSampleValueForType(field.getType(), 1);
                Object val2 = getSampleValueForType(field.getType(), 2);
                assertNotEquals(val0, val1);
                setter.invoke(p1, val0);
                setter.invoke(p2, val0);
                assertEquals(p1, p2);
                assertEquals(p1.hashCode(), p2.hashCode());
                assertEquals(getter.invoke(p1), getter.invoke(p2));
                assertEquals(getter.invoke(p1), val0);
                assertNotEquals(getter.invoke(p1), val1);
                hashCodes.add(p1.hashCode());
                setter.invoke(p1, val1);
                assertNotEquals("Field " + field + " is missing from equals()", p1, p2);
                assertNotEquals(getter.invoke(p1), getter.invoke(p2));
                assertNotEquals(getter.invoke(p1), val0);
                assertEquals(getter.invoke(p1), val1);
                setter.invoke(p1, val0);
                assertEquals("Field " + field + " setter might have side effects", p1, p2);
                assertEquals(p1.hashCode(), p2.hashCode());
                assertEquals(getter.invoke(p1), getter.invoke(p2));
                setter.invoke(p1, val1);
                setter.invoke(p2, val1);
                assertEquals(p1, p2);
                assertEquals(p1.hashCode(), p2.hashCode());
                assertEquals(getter.invoke(p1), getter.invoke(p2));
                setter.invoke(p1, val2);
                setter.invoke(p1, val1);
                assertEquals("Field " + field + " setter might have side effects", p1, p2);
                assertEquals(p1.hashCode(), p2.hashCode());
                assertEquals(getter.invoke(p1), getter.invoke(p2));
                hashCodes.add(p1.hashCode());

                // Check for clone behavior
                DecimalFormatProperties copy = p1.clone();
                assertEquals("Field " + field + " did not get copied in clone", p1, copy);
                assertEquals(p1.hashCode(), copy.hashCode());
                assertEquals(getter.invoke(p1), getter.invoke(copy));

                // Check for copyFrom behavior
                setter.invoke(p1, val0);
                assertNotEquals(p1, p2);
                assertNotEquals(getter.invoke(p1), getter.invoke(p2));
                p2.copyFrom(p1);
                assertEquals("Field " + field + " is missing from copyFrom()", p1, p2);
                assertEquals(p1.hashCode(), p2.hashCode());
                assertEquals(getter.invoke(p1), getter.invoke(p2));

                // Load values into p3 and p4 for clear() behavior test
                setter.invoke(p3, getSampleValueForType(field.getType(), 3));
                hashCodes.add(p3.hashCode());
                setter.invoke(p4, getSampleValueForType(field.getType(), 4));
                hashCodes.add(p4.hashCode());
            } catch (IllegalAccessException e) {
                fail("Could not access method for field " + field);
            } catch (IllegalArgumentException e) {
                fail("Could call method for field " + field);
            } catch (InvocationTargetException e) {
                fail("Could invoke method on target for field " + field);
            }
        }

        // Check for clear() behavior
        assertNotEquals(p3, p4);
        p3.clear();
        p4.clear();
        assertEquals("A field is missing from the clear() function", p3, p4);

        // A good hashCode() implementation should produce very few collisions. We added at most
        // 4*fields.length codes to the set. We'll say the implementation is good if we had at least
        // fields.length unique values.
        // TODO: Should the requirement be stronger than this?
        assertTrue(
                "Too many hash code collisions: " + hashCodes.size() + " out of " + (fields.length * 4),
                hashCodes.size() >= fields.length);
    }

    /**
     * Creates a valid sample instance of the given type. Used to simulate getters and setters.
     *
     * @param type
     *            The type to generate.
     * @param seed
     *            An integer seed, guaranteed to be positive. The same seed should generate two instances
     *            that are equal. A different seed should in general generate two instances that are not
     *            equal; this might not always be possible, such as with booleans or enums where there
     *            are limited possible values.
     * @return An instance of the specified type.
     */
    Object getSampleValueForType(Class<?> type, int seed) {
        if (type == Integer.TYPE) {
            return seed * 1000001;

        } else if (type == Boolean.TYPE) {
            return (seed % 2) == 0;

        } else if (type == BigDecimal.class) {
            if (seed == 0)
                return null;
            return new BigDecimal(seed * 1000002);

        } else if (type == String.class) {
            if (seed == 0)
                return null;
            return BigInteger.valueOf(seed * 1000003).toString(32);

        } else if (type == CompactStyle.class) {
            if (seed == 0)
                return null;
            CompactStyle[] values = CompactStyle.values();
            return values[seed % values.length];

        } else if (type == Currency.class) {
            if (seed == 0)
                return null;
            Object[] currencies = Currency.getAvailableCurrencies().toArray();
            return currencies[seed % currencies.length];

        } else if (type == CurrencyPluralInfo.class) {
            if (seed == 0)
                return null;
            ULocale[] locales = ULocale.getAvailableLocales();
            return CurrencyPluralInfo.getInstance(locales[seed % locales.length]);

        } else if (type == CurrencyUsage.class) {
            if (seed == 0)
                return null;
            CurrencyUsage[] values = CurrencyUsage.values();
            return values[seed % values.length];

        } else if (type == FormatWidth.class) {
            if (seed == 0)
                return null;
            FormatWidth[] values = FormatWidth.values();
            return values[seed % values.length];

        } else if (type == Map.class) {
            // Map<String,Map<String,String>> for compactCustomData property
            if (seed == 0)
                return null;
            Map<String, Map<String, String>> outer = new HashMap<String, Map<String, String>>();
            Map<String, String> inner = new HashMap<String, String>();
            inner.put("one", "0 thousand");
            StringBuilder magnitudeKey = new StringBuilder();
            magnitudeKey.append("1000");
            for (int i = 0; i < seed % 9; i++) {
                magnitudeKey.append("0");
            }
            outer.put(magnitudeKey.toString(), inner);
            return outer;

        } else if (type == MathContext.class) {
            if (seed == 0)
                return null;
            RoundingMode[] modes = RoundingMode.values();
            return new MathContext(seed, modes[seed % modes.length]);

        } else if (type == MeasureUnit.class) {
            if (seed == 0)
                return null;
            Object[] units = MeasureUnit.getAvailable().toArray();
            return units[seed % units.length];

        } else if (type == PadPosition.class) {
            if (seed == 0)
                return null;
            PadPosition[] values = PadPosition.values();
            return values[seed % values.length];

        } else if (type == ParseMode.class) {
            if (seed == 0)
                return null;
            ParseMode[] values = ParseMode.values();
            return values[seed % values.length];

        } else if (type == PluralRules.class) {
            if (seed == 0)
                return null;
            ULocale[] locales = PluralRules.getAvailableULocales();
            return PluralRules.forLocale(locales[seed % locales.length]);

        } else if (type == RoundingMode.class) {
            if (seed == 0)
                return null;
            RoundingMode[] values = RoundingMode.values();
            return values[seed % values.length];

        } else {
            fail("Don't know how to handle type "
                    + type
                    + ". Please add it to getSampleValueForType().");
            return null;
        }
    }

    @Test
    public void TestBasicSerializationRoundTrip() throws IOException, ClassNotFoundException {
        DecimalFormatProperties props0 = new DecimalFormatProperties();

        // Write values to some of the fields
        PatternStringParser.parseToExistingProperties("A-**####,#00.00#b¤", props0);

        // Write to byte stream
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(props0);
        oos.flush();
        baos.close();
        byte[] bytes = baos.toByteArray();

        // Read from byte stream
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Object obj = ois.readObject();
        ois.close();
        DecimalFormatProperties props1 = (DecimalFormatProperties) obj;

        // Test equality
        assertEquals("Did not round-trip through serialization", props0, props1);
    }

    /** Handler for serialization compatibility test suite. */
    public static class PropertiesHandler implements SerializableTestUtility.Handler {

        @Override
        public Object[] getTestObjects() {
            return new Object[] {
                    new DecimalFormatProperties(),
                    PatternStringParser.parseToProperties("x#,##0.00%"),
                    new DecimalFormatProperties().setCompactStyle(CompactStyle.LONG)
                            .setMinimumExponentDigits(2) };
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b) {
            return a.equals(b);
        }
    }

    /**
     * Handler for the ICU 59 class named "Properties" before it was renamed to
     * "DecimalFormatProperties".
     */
    public static class ICU59PropertiesHandler implements SerializableTestUtility.Handler {

        @Override
        public Object[] getTestObjects() {
            return new Object[] { new com.ibm.icu.impl.number.Properties() };
        }

        @Override
        public boolean hasSameBehavior(Object a, Object b) {
            return true;
        }
    }
}
