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

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

import java.math.RoundingMode;

import org.junit.Test;

import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.SkeletonSyntaxException;
import com.ibm.icu.util.ULocale;

/**
 * @author sffc
 *
 */
public class NumberSkeletonTest {

    @Test
    public void validTokens() {
        // This tests only if the tokens are valid, not their behavior.
        // Most of these are from the design doc.
        String[] cases = {
                "precision-integer",
                "precision-unlimited",
                "@@@##",
                "@@*",
                "@@+",
                ".000##",
                ".00*",
                ".00+",
                ".",
                ".*",
                ".+",
                ".######",
                ".00/@@*",
                ".00/@@+",
                ".00/@##",
                "precision-increment/3.14",
                "precision-currency-standard",
                "precision-integer rounding-mode-half-up",
                ".00# rounding-mode-ceiling",
                ".00/@@* rounding-mode-floor",
                ".00/@@+ rounding-mode-floor",
                "scientific",
                "scientific/*ee",
                "scientific/+ee",
                "scientific/sign-always",
                "scientific/*ee/sign-always",
                "scientific/+ee/sign-always",
                "scientific/sign-always/*ee",
                "scientific/sign-always/+ee",
                "scientific/sign-except-zero",
                "engineering",
                "engineering/*eee",
                "engineering/+eee",
                "compact-short",
                "compact-long",
                "notation-simple",
                "percent",
                "permille",
                "measure-unit/length-meter",
                "measure-unit/area-square-meter",
                "measure-unit/energy-joule per-measure-unit/length-meter",
                "unit/square-meter-per-square-meter",
                "currency/XXX",
                "currency/ZZZ",
                "currency/usd",
                "group-off",
                "group-min2",
                "group-auto",
                "group-on-aligned",
                "group-thousands",
                "integer-width/00",
                "integer-width/#0",
                "integer-width/*00",
                "integer-width/+00",
                "sign-always",
                "sign-auto",
                "sign-never",
                "sign-accounting",
                "sign-accounting-always",
                "sign-except-zero",
                "sign-accounting-except-zero",
                "unit-width-narrow",
                "unit-width-short",
                "unit-width-iso-code",
                "unit-width-full-name",
                "unit-width-hidden",
                "decimal-auto",
                "decimal-always",
                "scale/5.2",
                "scale/-5.2",
                "scale/100",
                "scale/1E2",
                "scale/1",
                "latin",
                "numbering-system/arab",
                "numbering-system/latn",
                "precision-integer/@##",
                "precision-integer rounding-mode-ceiling",
                "precision-currency-cash rounding-mode-ceiling",
                "0",
                "00",
                "000",
                "E0",
                "E00",
                "E000",
                "EE0",
                "EE00",
                "EE+?0",
                "EE+?00",
                "EE+!0",
                "EE+!00",
        };

        for (String cas : cases) {
            try {
                NumberFormatter.forSkeleton(cas);
            } catch (SkeletonSyntaxException e) {
                fail(e.getMessage());
            }
        }
    }

    @Test
    public void invalidTokens() {
        String[] cases = {
                ".00x",
                ".00##0",
                ".##*",
                ".00##*",
                ".0#*",
                "@#*",
                ".##+",
                ".00##+",
                ".0#+",
                "@#+",
                "@@x",
                "@@##0",
                ".00/@",
                ".00/@@",
                ".00/@@x",
                ".00/@@#",
                ".00/@@#*",
                ".00/floor/@@*", // wrong order
                ".00/@@#+",
                ".00/floor/@@+", // wrong order
                "precision-increment/français", // non-invariant characters for C++
                "scientific/ee",
                "precision-increment/xxx",
                "precision-increment/NaN",
                "precision-increment/0.1.2",
                "scale/xxx",
                "scale/NaN",
                "scale/0.1.2",
                "scale/français", // non-invariant characters for C++
                "currency/dummy",
                "currency/ççç", // three characters but not ASCII
                "measure-unit/foo",
                "integer-width/xxx",
                "integer-width/0*",
                "integer-width/*0#",
                "integer-width/*#",
                "integer-width/*#0",
                "integer-width/0+",
                "integer-width/+0#",
                "integer-width/+#",
                "integer-width/+#0",
                "scientific/foo",
                "E",
                "E1",
                "E+",
                "E+?",
                "E+!",
                "E+0",
                "EE",
                "EE+",
                "EEE",
                "EEE0",
                "001",
                "00*",
                "00+",
        };

        for (String cas : cases) {
            try {
                NumberFormatter.forSkeleton(cas);
                fail(cas);
            } catch (SkeletonSyntaxException expected) {
                assertTrue(expected.getMessage(), expected.getMessage().contains("Invalid"));
            }
        }
    }

    @Test
    public void unknownTokens() {
        String[] cases = {
                "maesure-unit",
                "measure-unit/foo-bar",
                "numbering-system/dummy",
                "français",
                "measure-unit/français-français", // non-invariant characters for C++
                "numbering-system/français", // non-invariant characters for C++
                "currency-USD" };

        for (String cas : cases) {
            try {
                NumberFormatter.forSkeleton(cas);
                fail(cas);
            } catch (SkeletonSyntaxException expected) {
                assertTrue(expected.getMessage(), expected.getMessage().contains("Unknown"));
            }
        }
    }

    @Test
    public void unexpectedTokens() {
        String[] cases = {
                "group-thousands/foo",
                "precision-integer//@## group-off",
                "precision-integer//@##  group-off",
                "precision-integer/ group-off",
                "precision-integer// group-off" };

        for (String cas : cases) {
            try {
                NumberFormatter.forSkeleton(cas);
                fail(cas);
            } catch (SkeletonSyntaxException expected) {
                assertTrue(expected.getMessage(), expected.getMessage().contains("Unexpected"));
            }
        }
    }

    @Test
    public void duplicateValues() {
        String[] cases = {
                "precision-integer precision-integer",
                "precision-integer .00+",
                "precision-integer precision-unlimited",
                "precision-integer @@@",
                "scientific engineering",
                "engineering compact-long",
                "sign-auto sign-always" };

        for (String cas : cases) {
            try {
                NumberFormatter.forSkeleton(cas);
                fail(cas);
            } catch (SkeletonSyntaxException expected) {
                assertTrue(expected.getMessage(), expected.getMessage().contains("Duplicated"));
            }
        }
    }

    @Test
    public void stemsRequiringOption() {
        String[] stems = {
                "precision-increment",
                "measure-unit",
                "per-measure-unit",
                "currency",
                "integer-width",
                "numbering-system",
                "scale" };
        String[] suffixes = { "", "/@##", " scientific", "/@## scientific" };

        for (String stem : stems) {
            for (String suffix : suffixes) {
                String skeletonString = stem + suffix;
                try {
                    NumberFormatter.forSkeleton(skeletonString);
                    fail(skeletonString);
                } catch (SkeletonSyntaxException expected) {
                    // Success
                }
            }
        }
    }

    @Test
    public void defaultTokens() {
        String[] cases = {
                "notation-simple",
                "base-unit",
                "group-auto",
                "integer-width/+0",
                "sign-auto",
                "unit-width-short",
                "decimal-auto" };

        for (String skeleton : cases) {
            String normalized = NumberFormatter.forSkeleton(skeleton).toSkeleton();
            assertEquals("Skeleton should become empty when normalized: " + skeleton, "", normalized);
        }
    }

    @Test
    public void flexibleSeparators() {
        String[][] cases = {
                { "precision-integer group-off", "5142" },
                { "precision-integer  group-off", "5142" },
                { "precision-integer/@## group-off", "5140" },
                { "precision-integer/@##  group-off", "5140" }, };

        for (String[] cas : cases) {
            String skeleton = cas[0];
            String expected = cas[1];
            String actual = NumberFormatter.forSkeleton(skeleton).locale(ULocale.ENGLISH).format(5142.3)
                    .toString();
            assertEquals(skeleton, expected, actual);
        }
    }

    @Test
    public void wildcardCharacters() {
        String[][] cases = {
            { ".00*", ".00+" },
            { "@@*", "@@+" },
            { ".00/@@*", ".00/@@+" },
            { "scientific/*ee", "scientific/+ee" },
            { "integer-width/*00", "integer-width/+00" },
        };

        for (String[] cas : cases) {
            String star = cas[0];
            String plus = cas[1];

            String normalized = NumberFormatter.forSkeleton(plus)
                .toSkeleton();
            assertEquals("Plus should normalize to star", star, normalized);
        }
    }

    @Test
    public void roundingModeNames() {
        for (RoundingMode mode : RoundingMode.values()) {
            if (mode == RoundingMode.HALF_EVEN) {
                // This rounding mode is not printed in the skeleton since it is the default
                continue;
            }
            String skeleton = NumberFormatter.with().roundingMode(mode).toSkeleton();
            String modeString = mode.toString().toLowerCase().replace('_', '-');
            assertEquals(mode.toString(), modeString, skeleton.substring(14));
        }
    }

    @Test
    public void perUnitInArabic() {
        String[][] cases = {
                {"area", "acre"},
                {"digital", "bit"},
                {"digital", "byte"},
                {"temperature", "celsius"},
                {"length", "centimeter"},
                {"duration", "day"},
                {"angle", "degree"},
                {"temperature", "fahrenheit"},
                {"volume", "fluid-ounce"},
                {"length", "foot"},
                {"volume", "gallon"},
                {"digital", "gigabit"},
                {"digital", "gigabyte"},
                {"mass", "gram"},
                {"area", "hectare"},
                {"duration", "hour"},
                {"length", "inch"},
                {"digital", "kilobit"},
                {"digital", "kilobyte"},
                {"mass", "kilogram"},
                {"length", "kilometer"},
                {"volume", "liter"},
                {"digital", "megabit"},
                {"digital", "megabyte"},
                {"length", "meter"},
                {"length", "mile"},
                {"length", "mile-scandinavian"},
                {"volume", "milliliter"},
                {"length", "millimeter"},
                {"duration", "millisecond"},
                {"duration", "minute"},
                {"duration", "month"},
                {"mass", "ounce"},
                {"concentr", "percent"},
                {"digital", "petabyte"},
                {"mass", "pound"},
                {"duration", "second"},
                {"mass", "stone"},
                {"digital", "terabit"},
                {"digital", "terabyte"},
                {"duration", "week"},
                {"length", "yard"},
                {"duration", "year"},
        };

        ULocale arabic = new ULocale("ar");
        for (String[] cas1 : cases) {
            for (String[] cas2 : cases) {
                String skeleton = "measure-unit/";
                skeleton += cas1[0] + "-" + cas1[1] + " per-measure-unit/" + cas2[0] + "-" + cas2[1];

                @SuppressWarnings("unused")
                String actual = NumberFormatter.forSkeleton(skeleton).locale(arabic).format(5142.3)
                        .toString();
                // Just make sure it won't throw exception
            }
        }
    }

    @Test
    public void perUnitToSkeleton() {
        String[][] cases = {
            {"area", "acre"},
            {"concentr", "percent"},
            {"concentr", "permille"},
            {"concentr", "permillion"},
            {"concentr", "permyriad"},
            {"digital", "bit"},
            {"length", "yard"},
        };

        for (String[] cas1 : cases) {
            for (String[] cas2 : cases) {
                String skeleton = "measure-unit/" + cas1[0] + "-" + cas1[1] + " per-measure-unit/" +
                                  cas2[0] + "-" + cas2[1];

                if (cas1[0] != cas2[0] && cas1[1] != cas2[1]) {
                    String toSkeleton = NumberFormatter.forSkeleton(skeleton).toSkeleton();

                    // Ensure both subtype are in the toSkeleton.
                    String msg;
                    msg = toSkeleton + " should contain '" + cas1[1] + "' when constructed from " +
                          skeleton;
                    assertTrue(msg, toSkeleton.indexOf(cas1[1]) >= 0);
                    msg = toSkeleton + " should contain '" + cas2[1] + "' when constructed from " +
                          skeleton;
                    assertTrue(msg, toSkeleton.indexOf(cas2[1]) >= 0);
                }
            }
        }
    }
}
