// Copyright 2007, 2008, 2009, 2010 The Apache Software Foundation
//
// Licensed 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.tapestry5.internal.services;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Locale;
import java.util.Map;

import org.apache.tapestry5.Field;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.internal.test.InternalBaseTestCase;
import org.apache.tapestry5.internal.translator.BigDecimalNumericFormatter;
import org.apache.tapestry5.internal.translator.BigIntegerNumericFormatter;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.services.ThreadLocale;
import org.apache.tapestry5.ioc.util.UnknownValueException;
import org.apache.tapestry5.services.TranslatorSource;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

@SuppressWarnings("unchecked")
public class TranslatorSourceImplTest extends InternalBaseTestCase
{
    private TranslatorSource source;

    @BeforeClass
    public void setup()
    {
        source = getService(TranslatorSource.class);
    }

    @BeforeMethod
    public void setupThreadLocale()
    {
        getService(ThreadLocale.class).setLocale(Locale.ENGLISH);
    }

    @Test
    public void found_translator_by_name()
    {
        Translator translator = mockTranslator("mock", String.class);

        replay();

        TranslatorSource source = new TranslatorSourceImpl(newConfiguration(String.class, translator));

        assertSame(source.get("mock"), translator);

        verify();
    }

    private Map<Class, Translator> newConfiguration(Class type, Translator t)
    {
        Map<Class, Translator> result = CollectionFactory.newMap();
        result.put(type, t);

        return result;
    }

    @Test
    public void key_and_type_mismatch()
    {
        Translator t = mockTranslator();

        train_getType(t, Long.class);

        replay();

        try
        {
            new TranslatorSourceImpl(newConfiguration(Integer.class, t));
            unreachable();
        }
        catch (RuntimeException ex)
        {
            assertMessageContains(ex,
                    "Contributed translator for type java.lang.Integer reports its type as java.lang.Long.");
        }

        verify();
    }

    @Test
    public void name_collision_with_standard_translators()
    {
        Translator t1 = mockTranslator("fred", Integer.class);
        Translator t2 = mockTranslator("fred", Long.class);

        Map<Class, Translator> configuration = CollectionFactory.newMap();
        configuration.put(Integer.class, t1);
        configuration.put(Long.class, t2);

        replay();

        try
        {
            new TranslatorSourceImpl(configuration);
            unreachable();
        }
        catch (RuntimeException ex)
        {
            assertMessageContains(
                    ex,
                    "Two different Translators contributed to the TranslatorSource service use the same translator name: 'fred'.",
                    "Translator names must be unique.");
        }

        verify();
    }

    @Test
    public void get_alternate_translator_by_name()
    {
        Translator t1 = mockTranslator("fred", Integer.class);
        Translator t2 = mockTranslator();

        Map<Class, Translator> configuration = newConfiguration(Integer.class, t1);

        Map<String, Translator> alternates = CollectionFactory.newMap();
        alternates.put("barney", t2);

        replay();

        TranslatorSource source = new TranslatorSourceImpl(configuration, alternates);

        assertSame(source.get("barney"), t2);

        verify();
    }

    @Test
    public void name_collision_between_standard_and_alternate_translator()
    {
        Translator t1 = mockTranslator("fred", Integer.class);
        Translator t2 = mockTranslator();

        Map<Class, Translator> configuration = newConfiguration(Integer.class, t1);

        Map<String, Translator> alternates = CollectionFactory.newMap();
        alternates.put("fred", t2);

        replay();

        try
        {
            new TranslatorSourceImpl(configuration, alternates);
            unreachable();
        }
        catch (RuntimeException ex)
        {
            assertEquals(
                    ex.getMessage(),
                    "Translator 'fred' contributed to the TranslatorAlternatesSource service has the same name as a standard Translator contributed to the TranslatorSource service.");
        }

        verify();
    }

    @Test
    public void unknown_translator_is_failure()
    {
        Translator fred = mockTranslator("fred", String.class);
        Translator barney = mockTranslator("barney", Long.class);

        Map<Class, Translator> configuration = CollectionFactory.newMap();

        configuration.put(String.class, fred);
        configuration.put(Long.class, barney);

        replay();

        TranslatorSource source = new TranslatorSourceImpl(configuration);

        try
        {
            source.get("wilma");
            unreachable();
        }
        catch (UnknownValueException ex)
        {
            assertMessageContains(ex, "Unknown translator type 'wilma'.");
        }
    }

    @DataProvider
    public Object[][] to_client_data()
    {
        return new Object[][]
        {

                { Byte.class, (byte) 65, "65" },

                { Integer.class, 997, "997" },

                { Long.class, 12345l, "12345" },

                // Is this a bug? We seem to be using a JDK- or locale-defined level of precision.
                // Maybe translators need room for configuration just like validators, so that
                // the correct decimal format string could be specified in the message catalog.

                { Double.class, 3.1428571429d, "3.143" },

                { String.class, "abcd", "abcd" },

                { Short.class, (short) 95, "95" },

                { Float.class, (float) -22.7, "-22.7" },

                { BigInteger.class, new BigInteger("123456789012345678901234567890"), "123456789012345678901234567890" },

                { BigDecimal.class, new BigDecimal("-9876543219876543321987654321.12345123451234512345"),
                        "-9876543219876543321987654321.12345123451234512345" } };
    }

    @Test(dataProvider = "to_client_data")
    public void to_client(Class type, Object value, String expected)
    {
        Translator t = source.getByType(type);

        String actual = t.toClient(value);

        assertEquals(actual, expected);
    }

    @DataProvider
    public Object[][] parse_client_success_data()
    {
        return new Object[][]
        {

                { Byte.class, " 23 ", (byte) 23 },

                { Short.class, " -121 ", (short) -121 },

                { Integer.class, " 123 ", 123 },

                { Integer.class, " 20,000 ", 20000 },

                { Long.class, "  -1234567 ", -1234567l },

                { Double.class, "3.1428571429", 3.1428571429d },

                { String.class, " abcdef ", " abcdef " },

                { Float.class, " 28.95 ", (float) 28.95 },

                { BigInteger.class, " -123456789012345678901234567890",
                        new BigInteger("-123456789012345678901234567890") },

                { BigDecimal.class, "-9,876,543,219,876,543,321,987,654,321.12345123451234512345",
                        new BigDecimal("-9876543219876543321987654321.12345123451234512345") } };
    }

    @Test(dataProvider = "parse_client_success_data")
    public void parse_client(Class type, String input, Object expected) throws Exception
    {
        Translator t = source.getByType(type);

        Object actual = t.parseClient(null, input, null);

        assertEquals(actual, expected);
    }

    @DataProvider
    public Object[][] parse_client_failure_data()
    {
        String intError = "You must provide an integer value for Fred.";
        String floatError = "You must provide a numeric value for Fred.";

        return new Object[][]
        {

        { Byte.class, "fred", intError },

        { Integer.class, "fred", intError },

        { Long.class, "fred", intError },

        { Double.class, "fred", floatError },

        { Float.class, "fred", floatError },

        { Short.class, "fred", intError } };
    }

    @Test(dataProvider = "parse_client_failure_data")
    public void parse_client_failure(Class type, String input, String expectedMessage)
    {
        Translator t = source.getByType(type);
        Field field = mockField();

        replay();

        try
        {
            t.parseClient(field, input, expectedMessage);
            unreachable();
        }
        catch (ValidationException ex)
        {
            assertEquals(ex.getMessage(), expectedMessage);
        }

        verify();
    }

    @Test
    public void find_by_type()
    {
        Translator t = mockTranslator("string", String.class);

        replay();

        TranslatorSource source = new TranslatorSourceImpl(newConfiguration(String.class, t));

        assertSame(source.getByType(String.class), t);
        assertSame(source.findByType(String.class), t);
        assertNull(source.findByType(Integer.class));

        verify();
    }

    @Test
    public void get_by_type_not_found()
    {
        Translator string = mockTranslator("string", String.class);
        Translator bool = mockTranslator("bool", Boolean.class);

        Map<Class, Translator> configuration = CollectionFactory.newMap();
        configuration.put(String.class, string);
        configuration.put(Boolean.class, bool);

        replay();

        TranslatorSource source = new TranslatorSourceImpl(configuration);

        try
        {
            source.getByType(Integer.class);
            unreachable();
        }
        catch (IllegalArgumentException ex)
        {
            assertEquals(ex.getMessage(),
                    "No translator is defined for type java.lang.Integer.  Registered types: java.lang.Boolean, java.lang.String.");
        }

        verify();
    }

    @Test
    public void biginteger_with_localized_symbols() throws ParseException
    {
        DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.ENGLISH);
        symbols.setGroupingSeparator('_');
        symbols.setMinusSign('*');

        BigIntegerNumericFormatter f = new BigIntegerNumericFormatter(symbols);

        BigInteger big = new BigInteger("-123456");

        assertEquals(f.parse("*123_456"), big);

        assertEquals(f.toClient(big), "*123456");
    }

    @Test
    public void bigdecimal_with_localized_symbols() throws ParseException
    {
        DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.ENGLISH);
        symbols.setGroupingSeparator('_');
        symbols.setMinusSign('*');
        symbols.setDecimalSeparator('#');

        BigDecimalNumericFormatter f = new BigDecimalNumericFormatter(symbols);

        BigDecimal big = new BigDecimal("-123456.797956563434");

        assertEquals(f.parse("*123_456#797956563434"), big);

        assertEquals(f.toClient(big), "*123456#797956563434");
    }

}
