/*
 * Copyright (c) 2016-2019 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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.eclipse.configjsr;

import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;

import javax.config.Config;
import javax.config.inject.ConfigProperty;
import javax.config.spi.ConfigProviderResolver;
import javax.config.spi.ConfigSource;
import javax.config.spi.ConfigSourceProvider;
import javax.config.spi.Converter;
import javax.inject.Inject;

import org.eclipse.configjsr.base.AbstractTest;
import org.eclipse.configjsr.configsources.CustomConfigSourceProvider;
import org.eclipse.configjsr.configsources.CustomDbConfigSource;
import org.eclipse.configjsr.converters.Donald;
import org.eclipse.configjsr.converters.Duck;
import org.eclipse.configjsr.converters.DuckConverter;
import org.eclipse.configjsr.converters.SomeEnumToConvert;
import org.eclipse.configjsr.converters.UpperCaseDuckConverter;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.testng.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.testng.Assert;
import org.testng.annotations.Test;


/**
 * @author <a href="mailto:[email protected]">Mark Struberg</a>
 * @author <a href="mailto:[email protected]">Emily Jiang</a>
 * @author <a href="mailto:[email protected]">John D. Ament</a>
 */
public class ConverterTest extends Arquillian {

    private @Inject Config config;

    @Deployment
    public static WebArchive deploy() {
        JavaArchive testJar = ShrinkWrap
                .create(JavaArchive.class, "converterTest.jar")
                .addClass(ConverterTest.class)
                .addPackage(CustomDbConfigSource.class.getPackage())
                .addClasses(DuckConverter.class, Duck.class, Donald.class, SomeEnumToConvert.class)
                .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")
                .addAsServiceProvider(ConfigSource.class, CustomDbConfigSource.class)
                .addAsServiceProvider(ConfigSourceProvider.class, CustomConfigSourceProvider.class)
                .addAsServiceProvider(Converter.class, DuckConverter.class)
                .as(JavaArchive.class);

        AbstractTest.addFile(testJar, "META-INF/javaconfig.properties");
        AbstractTest.addFile(testJar, "sampleconfig.yaml");

        WebArchive war = ShrinkWrap
                .create(WebArchive.class, "converterTest.war")
                .addAsLibrary(testJar);
        return war;
    }

    @Inject
    @ConfigProperty(name = "tck.config.test.javaconfig.converter.duckname")
    private Duck namedDuck;

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testDonaldNotConvertedByDefault() {
        config.getValue("tck.config.test.javaconfig.converter.donaldname", Donald.class);
    }

    @Test
    public void testDonaldConversionWithLambdaConverter() {
        Config newConfig = ConfigProviderResolver.instance().getBuilder().addDefaultSources()
            .withConverter(Donald.class, 100, (s) -> Donald.iLikeDonald(s))
            .build();
        Donald donald = newConfig.getValue("tck.config.test.javaconfig.converter.donaldname", Donald.class);
        Assert.assertNotNull(donald);
        Assert.assertEquals(donald.getName(), "Duck");
    }

    @Test
    public void testDonaldConversionWithMultipleLambdaConverters() {
        // defines 2 config with the lambda converters defined in different orders.
        // Order must not matter, the lambda with the upper case must always be used as it has the highest priority
        Config config1 = ConfigProviderResolver.instance().getBuilder().addDefaultSources()
            .withConverter(Donald.class, 101, (s) -> Donald.iLikeDonald(s.toUpperCase()))
            .withConverter(Donald.class, 100, (s) -> Donald.iLikeDonald(s))
            .build();
        Config config2 = ConfigProviderResolver.instance().getBuilder().addDefaultSources()
            .withConverter(Donald.class, 100, (s) -> Donald.iLikeDonald(s))
            .withConverter(Donald.class, 101, (s) -> Donald.iLikeDonald(s.toUpperCase()))
            .build();

        Donald donald = config1.getValue("tck.config.test.javaconfig.converter.donaldname", Donald.class);
        Assert.assertNotNull(donald);
        Assert.assertEquals(donald.getName(), "DUCK",
            "The converter with the highest priority (using upper case) must be used.");
        donald = config2.getValue("tck.config.test.javaconfig.converter.donaldname", Donald.class);
        Assert.assertNotNull(donald);
        Assert.assertEquals(donald.getName(), "DUCK",
            "The converter with the highest priority (using upper case) must be used.");
    }

    @Test
    public void testEnum() {
        SomeEnumToConvert value = config.getValue("tck.config.test.javaconfig.converter.implicit.enumvalue", SomeEnumToConvert.class);
        Assert.assertEquals(value, SomeEnumToConvert.FOO);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testEnum_Broken() {
        config.getValue("tck.config.test.javaconfig.converter.implicit.enumvalue.broken", SomeEnumToConvert.class);
    }


    @Test
    public void testInteger() {
        Integer value = config.getValue("tck.config.test.javaconfig.converter.integervalue", Integer.class);
        Assert.assertEquals(value, Integer.valueOf(1234));
    }

    @Test
    public void testInt() {
        int value = config.getValue("tck.config.test.javaconfig.converter.integervalue", int.class);
        Assert.assertEquals(value, 1234);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testInteger_Broken() {
        Integer value = config.getValue("tck.config.test.javaconfig.converter.integervalue.broken", Integer.class);
    }

    @Test
    public void testLong() {
        Long value = config.getValue("tck.config.test.javaconfig.converter.longvalue", Long.class);
        Assert.assertEquals(value, Long.valueOf(1234567890));
    }

    @Test
    public void testlong() {
        long primitiveValue = config.getValue("tck.config.test.javaconfig.converter.longvalue", long.class);
        Assert.assertEquals(primitiveValue, 1234567890L);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testLong_Broken() {
        config.getValue("tck.config.test.javaconfig.converter.longvalue.broken", Long.class);
    }

    @Test
    public void testFloat() {
        Float value = config.getValue("tck.config.test.javaconfig.converter.floatvalue", Float.class);
        Assert.assertEquals(value, 12.34f);
    }

    @Test
    public void testfloat() {
        float value = config.getValue("tck.config.test.javaconfig.converter.floatvalue", float.class);
        Assert.assertEquals(value, 12.34f);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testFloat_Broken() {
        config.getValue("tck.config.test.javaconfig.converter.floatvalue.broken", Float.class);
    }

    @Test
    public void testDouble() {
        Double value = config.getValue("tck.config.test.javaconfig.converter.doublevalue", Double.class);
        Assert.assertEquals(value, 12.34d);
    }

    @Test
    public void testdouble() {
        double value = config.getValue("tck.config.test.javaconfig.converter.doublevalue", double.class);
        Assert.assertEquals(value,12.34d);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testDouble_Broken() {
        Double value = config.getValue("tck.config.test.javaconfig.converter.doublevalue.broken", Double.class);
    }

    @Test
    public void testDuration() {
        Duration value = config.getValue("tck.config.test.javaconfig.converter.durationvalue", Duration.class);
        Assert.assertEquals(value, Duration.parse("PT15M"));
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testDuration_Broken() {
        Duration value = config.getValue("tck.config.test.javaconfig.converter.durationvalue.broken", Duration.class);
    }

    @Test
    public void testLocalTime() {
        LocalTime value = config.getValue("tck.config.test.javaconfig.converter.localtimevalue", LocalTime.class);
        Assert.assertEquals(value, LocalTime.parse("10:37"));
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testLocalTime_Broken() {
        LocalTime value = config.getValue("tck.config.test.javaconfig.converter.localtimevalue.broken", LocalTime.class);
    }

    @Test
    public void testLocalDate() {
        LocalDate value = config.getValue("tck.config.test.javaconfig.converter.localdatevalue", LocalDate.class);
        Assert.assertEquals(value, LocalDate.parse("2017-12-24"));
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testLocalDate_Broken() {
        LocalDate value = config.getValue("tck.config.test.javaconfig.converter.localdatevalue.broken", LocalDate.class);
    }

    @Test
    public void testLocalDateTime() {
        LocalDateTime value = config.getValue("tck.config.test.javaconfig.converter.localdatetimevalue", LocalDateTime.class);
        Assert.assertEquals(value, LocalDateTime.parse("2017-12-24T10:25:30"));
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testLocalDateTime_Broken() {
        LocalDateTime value = config.getValue("tck.config.test.javaconfig.converter.localdatetimevalue.broken", LocalDateTime.class);
    }

    @Test
    public void testOffsetDateTime() {
        OffsetDateTime value = config.getValue("tck.config.test.javaconfig.converter.offsetdatetimevalue", OffsetDateTime.class);
        Assert.assertEquals(value, OffsetDateTime.parse("2007-12-03T10:15:30+01:00"));
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testOffsetDateTime_Broken() {
        OffsetDateTime value = config.getValue("tck.config.test.javaconfig.converter.offsetdatetimevalue.broken", OffsetDateTime.class);
    }

    @Test
    public void testOffsetTime() {
        OffsetTime value = config.getValue("tck.config.test.javaconfig.converter.offsettimevalue", OffsetTime.class);
        OffsetTime parsed = OffsetTime.parse("13:45:30.123456789+02:00");
        Assert.assertEquals(value, parsed);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testOffsetTime_Broken() {
        OffsetTime value = config.getValue("tck.config.test.javaconfig.converter.offsettimevalue.broken", OffsetTime.class);
    }

    @Test
    public void testInstant() {
        Instant value = config.getValue("tck.config.test.javaconfig.converter.instantvalue", Instant.class);
        Assert.assertEquals(value, Instant.parse("2015-06-02T21:34:33.616Z"));
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testInstant_Broken() {
        Instant value = config.getValue("tck.config.test.javaconfig.converter.instantvalue.broken", Instant.class);
    }

    @Test
    public void testBoolean() {
        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.true", Boolean.class));
        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.true", boolean.class));
        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.true_uppercase", Boolean.class));
        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.true_mixedcase", Boolean.class));
        Assert.assertFalse(config.getValue("tck.config.test.javaconfig.configvalue.boolean.false", Boolean.class));

        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.one", Boolean.class));
        Assert.assertFalse(config.getValue("tck.config.test.javaconfig.configvalue.boolean.zero", Boolean.class));
        Assert.assertFalse(config.getValue("tck.config.test.javaconfig.configvalue.boolean.seventeen", Boolean.class));

        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.yes", Boolean.class));
        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.yes_uppercase", Boolean.class));
        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.yes_mixedcase", Boolean.class));
        Assert.assertFalse(config.getValue("tck.config.test.javaconfig.configvalue.boolean.no", Boolean.class));

        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.y", Boolean.class));
        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.y_uppercase", Boolean.class));
        Assert.assertFalse(config.getValue("tck.config.test.javaconfig.configvalue.boolean.n", Boolean.class));

        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.on", Boolean.class));
        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.on_uppercase", Boolean.class));
        Assert.assertTrue(config.getValue("tck.config.test.javaconfig.configvalue.boolean.on_mixedcase", Boolean.class));
        Assert.assertFalse(config.getValue("tck.config.test.javaconfig.configvalue.boolean.off", Boolean.class));
        Assert.assertFalse(config.getValue("tck.config.test.javaconfig.configvalue.boolean.off", boolean.class));
    }

    @Test
    public void testCustomConverter() {
        Assert.assertEquals(namedDuck.getName(), "Hannelore");
    }

    @Test
    public void testDuckConversionWithMultipleConverters() {
        // defines 2 config with the converters defined in different orders.
        // Order must not matter, the UpperCaseDuckConverter must always be used as it has the highest priority
        Config config1 = ConfigProviderResolver.instance().getBuilder().addDefaultSources()
            .withConverters(new UpperCaseDuckConverter(), new DuckConverter())
            .build();
        Config config2 = ConfigProviderResolver.instance().getBuilder().addDefaultSources()
            .withConverters(new DuckConverter(), new UpperCaseDuckConverter())
            .build();

        Duck duck = config1.getValue("tck.config.test.javaconfig.converter.duckname", Duck.class);
        Assert.assertNotNull(duck);
        Assert.assertEquals(duck.getName(), "HANNELORE",
            "The converter with the highest priority (UpperCaseDuckConverter) must be used.");

        duck = config2.getValue("tck.config.test.javaconfig.converter.duckname", Duck.class);
        Assert.assertNotNull(duck);
        // the UpperCaseDuckConverter has highest priority
        Assert.assertEquals(duck.getName(), "HANNELORE",
            "The converter with the highest priority (UpperCaseDuckConverter) must be used.");
    }

    @Test
    public void testURLConverter() throws MalformedURLException {
        URL url = config.getValue("tck.config.test.javaconfig.converter.urlvalue", URL.class);
        Assert.assertEquals(url, new URL("http://microprofile.io"));
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testURLConverterBroken() throws Exception {
        URL ignored = config.getValue("tck.config.test.javaconfig.converter.urlvalue.broken", URL.class);
    }

    @Test
    public void testConverterAutoClose() {
        DuckConverter duckConverter = new DuckConverter();

        Assert.assertEquals(duckConverter.getCloseCounter(), 0);

        Config config = ConfigProviderResolver.instance().getBuilder()
            .withConverters(duckConverter)
            .build();

        // just to trigger the config
        config.getOptionalValue("somekey", String.class);

        ConfigProviderResolver.instance().releaseConfig(config);

        Assert.assertEquals(duckConverter.getCloseCounter(), 1);
    }

}