package io.smallrye.config;

import static io.smallrye.config.ConfigMapping.NamingStrategy.VERBATIM;
import static io.smallrye.config.ConfigMappingInterfaceTest.HyphenatedEnumMapping.HyphenatedEnum.VALUE_ONE;
import static io.smallrye.config.ConfigMappingInterfaceTest.MapKeyEnum.ClientId.NAF;
import static io.smallrye.config.ConfigMappingInterfaceTest.MapKeyEnum.ClientId.SOS_DAH;
import static io.smallrye.config.ConfigMappings.ConfigClass.configClass;
import static io.smallrye.config.KeyValuesConfigSource.config;
import static io.smallrye.config.SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
import static java.util.stream.StreamSupport.stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.microprofile.config.spi.Converter;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.smallrye.config.ConfigMappingInterfaceTest.MyRestClientConfig.RestClientConfig;
import io.smallrye.config.common.MapBackedConfigSource;

class ConfigMappingInterfaceTest {
    @Test
    void configMapping() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(configClass(Server.class, "server"))
                .withSources(config("server.host", "localhost", "server.port", "8080")).build();
        Server configProperties = config.getConfigMapping(Server.class, "server");
        assertEquals("localhost", configProperties.host());
        assertEquals(8080, configProperties.port());
    }

    @Test
    void noConfigMapping() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("server.host", "localhost", "server.port", "8080")).build();
        NoSuchElementException exception = assertThrows(NoSuchElementException.class,
                () -> config.getConfigMapping(Server.class, "server"));
        assertEquals("SRCFG00027: Could not find a mapping for " + Server.class.getName(), exception.getMessage());
    }

    @Test
    void unregisteredConfigMapping() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("host", "localhost", "port", "8080")).build();
        NoSuchElementException exception = assertThrows(NoSuchElementException.class,
                () -> config.getConfigMapping(Server.class));
        assertEquals("SRCFG00027: Could not find a mapping for " + Server.class.getName(), exception.getMessage());
    }

    @Test
    void unregistedPrefix() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(Server.class)
                .withSources(config("host", "localhost", "port", "8080")).build();
        NoSuchElementException exception = assertThrows(NoSuchElementException.class,
                () -> config.getConfigMapping(Server.class, "server"));
        assertEquals("SRCFG00028: Could not find a mapping for " + Server.class.getName() + " with prefix server",
                exception.getMessage());
    }

    @Test
    void noPrefix() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(Server.class)
                .withSources(config("host", "localhost", "port", "8080")).build();
        Server configProperties = config.getConfigMapping(Server.class);
        assertEquals("localhost", configProperties.host());
        assertEquals(8080, configProperties.port());
    }

    @Test
    void unknownConfigElement() {
        assertThrows(ConfigValidationException.class,
                () -> new SmallRyeConfigBuilder().withMapping(configClass(Server.class, "server")).build());
    }

    @Test
    void ignorePropertiesInUnregisteredRoots() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(configClass(Server.class, "server"))
                .withSources(config("server.host", "localhost", "server.port", "8080", "client.name", "konoha"))
                .build();
        Server configProperties = config.getConfigMapping(Server.class, "server");
        assertEquals("localhost", configProperties.host());
        assertEquals(8080, configProperties.port());
    }

    @Test
    void ignoreSomeProperties() {
        final SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(configClass(Server.class, "server"))
                .withMapping(configClass(Client.class, "client"))
                .withMappingIgnore("client.**")
                .withSources(config("server.host", "localhost", "server.port", "8080", "client.host", "localhost",
                        "client.port", "8080", "client.name", "konoha"))
                .build();

        Server server = config.getConfigMapping(Server.class, "server");
        assertEquals("localhost", server.host());
        assertEquals(8080, server.port());

        Client client = config.getConfigMapping(Client.class, "client");
        assertEquals("localhost", client.host());
        assertEquals(8080, client.port());
    }

    @Test
    void ignoreProperties() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .addDefaultSources()
                .withMapping(configClass(Server.class, "server"))
                .withSources(config("server.host", "localhost", "server.port", "8080")).build();
        Server configProperties = config.getConfigMapping(Server.class, "server");
        assertEquals("localhost", configProperties.host());
        assertEquals(8080, configProperties.port());
    }

    @Test
    void validateUnknown() {
        assertThrows(ConfigValidationException.class,
                () -> new SmallRyeConfigBuilder().addDefaultSources().withMapping(Server.class).build());

        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .addDefaultSources()
                .withMapping(Server.class)
                .withMapping(configClass(Server.class, "server"))
                .withValidateUnknown(false)
                .withSources(config("server.host", "localhost", "server.port", "8080", "host", "localhost", "port", "8080"))
                .build();

        Server configProperties = config.getConfigMapping(Server.class);
        assertEquals("localhost", configProperties.host());
        assertEquals(8080, configProperties.port());
    }

    @Test
    void splitRoots() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("server.host", "localhost", "server.port", "8080", "server.name", "konoha"))
                .withMapping(configClass(SplitRootServerHostAndPort.class, "server"))
                .withMapping(configClass(SplitRootServerName.class, "server"))
                .build();

        SplitRootServerHostAndPort server = config.getConfigMapping(SplitRootServerHostAndPort.class, "server");
        assertEquals("localhost", server.host());
        assertEquals(8080, server.port());

        SplitRootServerName name = config.getConfigMapping(SplitRootServerName.class, "server");
        assertEquals("konoha", name.name());
    }

    @Test
    void splitRootsInConfig() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("server.host", "localhost", "server.port", "8080", "server.name",
                        "konoha"))
                .withMapping(configClass(SplitRootServerHostAndPort.class, "server"))
                .withMapping(configClass(SplitRootServerName.class, "server"))
                .build();
        SplitRootServerHostAndPort server = config.getConfigMapping(SplitRootServerHostAndPort.class, "server");
        assertEquals("localhost", server.host());
        assertEquals(8080, server.port());
    }

    @Test
    void subGroups() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("server.host", "localhost", "server.port", "8080", "server.name",
                        "konoha"))
                .withMapping(configClass(ServerSub.class, "server"))
                .build();
        ServerSub server = config.getConfigMapping(ServerSub.class, "server");
        assertEquals("localhost", server.subHostAndPort().host());
        assertEquals(8080, server.subHostAndPort().port());
        assertEquals("konoha", server.subName().name());
    }

    @Test
    void types() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config(
                        "int", "9",
                        "long", "9999999999",
                        "float", "99.9",
                        "double", "99.99",
                        "char", "c",
                        "boolean", "true"))
                .withMapping(SomeTypes.class)
                .build();
        SomeTypes types = config.getConfigMapping(SomeTypes.class);

        assertEquals(9, types.intPrimitive());
        assertEquals(9, types.intWrapper());
        assertEquals(9999999999L, types.longPrimitive());
        assertEquals(9999999999L, types.longWrapper());
        assertEquals(99.9f, types.floatPrimitive());
        assertEquals(99.9f, types.floatWrapper());
        assertEquals(99.99, types.doublePrimitive());
        assertEquals(99.99, types.doubleWrapper());
        assertEquals('c', types.charPrimitive());
        assertEquals('c', types.charWrapper());
        assertTrue(types.booleanPrimitive());
        assertTrue(types.booleanWrapper());
    }

    @Test
    void optionals() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config(
                        "server.host", "localhost",
                        "server.port", "8080",
                        "optional", "optional",
                        "optional.int", "9",
                        "info.name", "server",
                        "info.login", "login",
                        "info.password", "password"))
                .withMapping(Optionals.class)
                .build();
        Optionals optionals = config.getConfigMapping(Optionals.class);

        assertTrue(optionals.server().isPresent());
        assertEquals("localhost", optionals.server().get().host());
        assertEquals(8080, optionals.server().get().port());

        assertTrue(optionals.optional().isPresent());
        assertEquals("optional", optionals.optional().get());
        assertTrue(optionals.optionalInt().isPresent());
        assertEquals(9, optionals.optionalInt().getAsInt());

        assertEquals(1, optionals.info().size());
        assertEquals("server", optionals.info().get("info").name());
        assertTrue(optionals.info().get("info").login().isPresent());
        assertEquals("login", optionals.info().get("info").login().get());
        assertTrue(optionals.info().get("info").password().isPresent());
        assertEquals("password", optionals.info().get("info").password().get());
    }

    @Test
    void collectionTypes() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("strings", "foo,bar", "ints", "1,2,3"))
                .withMapping(CollectionTypes.class)
                .build();
        CollectionTypes types = config.getConfigMapping(CollectionTypes.class);

        assertEquals(Stream.of("foo", "bar").collect(toList()), types.listStrings());
        assertEquals(Stream.of(1, 2, 3).collect(toList()), types.listInts());
    }

    @Test
    void maps() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config(
                        "server.host", "localhost",
                        "server.port", "8080",
                        "server.group.server.host", "localhost-group",
                        "server.group.server.port", "8081",
                        "server.server.host", "localhost-server",
                        "server.server.port", "8082"))
                .withMapping(Maps.class)
                .build();
        Maps maps = config.getConfigMapping(Maps.class);

        assertEquals(6, maps.server().size());
        assertEquals("localhost", maps.server().get("host"));
        assertEquals(8080, Integer.valueOf(maps.server().get("port")));

        assertEquals(1, maps.group().size());
        assertEquals("localhost-group", maps.group().get("server").host());
        assertEquals(8081, maps.group().get("server").port());

        assertEquals(1, maps.groupParentName().size());
        assertEquals("localhost-server", maps.groupParentName().get("server").host());
        assertEquals(8082, maps.groupParentName().get("server").port());
    }

    @Test
    void mapsEmptyPrefix() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config(
                        "host", "localhost",
                        "port", "8080",
                        "group.server.host", "localhost",
                        "group.server.port", "8081",
                        "server.host", "localhost",
                        "server.port", "8082"))
                .withMapping(configClass(Maps.class, ""))
                .build();
        Maps maps = config.getConfigMapping(Maps.class, "");

        assertEquals(6, maps.server().size());
        assertEquals("localhost", maps.server().get("host"));
        assertEquals(8080, Integer.valueOf(maps.server().get("port")));

        assertEquals(1, maps.group().size());
        assertEquals("localhost", maps.group().get("server").host());
        assertEquals(8081, maps.group().get("server").port());

        assertEquals(1, maps.groupParentName().size());
        assertEquals("localhost", maps.groupParentName().get("server").host());
        assertEquals(8082, maps.groupParentName().get("server").port());
    }

    @Test
    void converters() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("foo", "notbar"))
                .withMapping(Converters.class)
                .build();
        Converters converters = config.getConfigMapping(Converters.class);

        assertEquals("bar", converters.foo());
        assertTrue(converters.bprim());
        assertEquals('c', converters.cprim());
        assertEquals(-1, converters.iprim());
        assertEquals(-1, converters.sprim());
        assertEquals(-1L, converters.lprim());
        assertEquals(-1.0, converters.fprim());
        assertEquals(-1.0, converters.dprim());
    }

    @Test
    void mix() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config(
                        "server.host", "localhost",
                        "server.port", "8080",
                        "server.name", "server",
                        "client.host", "clienthost",
                        "client.port", "80",
                        "client.name", "client"))
                .withMapping(ComplexSample.class)
                .build();

        ComplexSample sample = config.getConfigMapping(ComplexSample.class);
        assertEquals("localhost", sample.server().subHostAndPort().host());
        assertEquals(8080, sample.server().subHostAndPort().port());
        assertTrue(sample.client().isPresent());
        assertEquals("clienthost", sample.client().get().subHostAndPort().host());
        assertEquals(80, sample.client().get().subHostAndPort().port());
    }

    @Test
    void noDynamicValues() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(configClass(Server.class, "server"))
                .withSources(config("server.host", "localhost", "server.port", "8080"))
                .withSources(new MapBackedConfigSource("test", new HashMap<>(), Integer.MAX_VALUE) {
                    private int counter = 1;

                    @Override
                    public String getValue(String propertyName) {
                        return counter++ + "";
                    }
                }).build();

        Server server = config.getConfigMapping(Server.class, "server");

        assertNotEquals(config.getConfigValue("server.port").getValue(), config.getConfigValue("server.port").getValue());
        assertEquals(server.port(), server.port());
    }

    @Test
    void mapClass() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(configClass(ServerClass.class, "server"))
                .withSources(config("server.host", "localhost", "server.port", "8080")).build();
        ServerClass server = config.getConfigMapping(ServerClass.class, "server");
        assertEquals("localhost", server.getHost());
        assertEquals(8080, server.getPort());
    }

    static class ServerClass {
        String host;
        int port;

        String getHost() {
            return host;
        }

        int getPort() {
            return port;
        }
    }

    @Test
    void configMappingAnnotation() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(configClass(ServerAnnotated.class, "server"))
                .withMapping(configClass(ServerAnnotated.class, "cloud"))
                .withSources(
                        config("server.host", "localhost", "server.port", "8080", "cloud.host", "cloud", "cloud.port", "9090"))
                .build();

        ServerAnnotated server = config.getConfigMapping(ServerAnnotated.class, "server");
        assertNotNull(server);
        assertEquals("localhost", server.host());
        assertEquals(8080, server.port());

        ServerAnnotated cloud = config.getConfigMapping(ServerAnnotated.class);
        assertNotNull(cloud);
        assertEquals("cloud", cloud.host());
        assertEquals(9090, cloud.port());

        ServerAnnotated cloudNull = config.getConfigMapping(ServerAnnotated.class, null);
        assertNotNull(cloudNull);
        assertEquals("cloud", cloudNull.host());
        assertEquals(9090, cloudNull.port());
    }

    @Test
    void prefixFromAnnotation() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(ServerAnnotated.class)
                .withSources(config("cloud.host", "cloud", "cloud.port", "9090"))
                .build();

        ServerAnnotated cloud = config.getConfigMapping(ServerAnnotated.class);
        assertNotNull(cloud);
        assertEquals("cloud", cloud.host());
        assertEquals(9090, cloud.port());
    }

    @Test
    void superTypes() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(configClass(ServerChild.class, "server"))
                .withSources(config("server.host", "localhost", "server.port", "8080"))
                .build();

        ServerChild server = config.getConfigMapping(ServerChild.class, "server");
        assertNotNull(server);
        assertEquals("localhost", server.host());
        assertEquals(8080, server.port());
    }

    @Test
    void configValue() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(ServerConfigValue.class)
                .withSources(config("server.host", "localhost", "server.port", "8080"))
                .build();

        ServerConfigValue server = config.getConfigMapping(ServerConfigValue.class, "server");
        assertNotNull(server);
        assertEquals("localhost", server.host().getValue());
        assertEquals(8080, Integer.valueOf(server.port().getValue()));
    }

    @Test
    void empty() {
        try {
            new SmallRyeConfigBuilder().withMapping(Empty.class).build();
        } catch (Exception e) {
            Assertions.fail(e);
        }
    }

    interface Server {
        String host();

        int port();
    }

    interface Client {
        String host();

        int port();
    }

    interface SplitRootServerHostAndPort {
        String host();

        int port();
    }

    interface SplitRootServerName {
        String name();
    }

    interface ServerSub {
        @WithParentName
        ServerSubHostAndPort subHostAndPort();

        @WithParentName
        ServerSubName subName();
    }

    interface ServerSubHostAndPort {
        String host();

        int port();
    }

    interface ServerSubName {
        String name();
    }

    interface SomeTypes {
        @WithName("int")
        int intPrimitive();

        @WithName("int")
        Integer intWrapper();

        @WithName("long")
        long longPrimitive();

        @WithName("long")
        Long longWrapper();

        @WithName("float")
        float floatPrimitive();

        @WithName("float")
        Float floatWrapper();

        @WithName("double")
        double doublePrimitive();

        @WithName("double")
        Double doubleWrapper();

        @WithName("char")
        char charPrimitive();

        @WithName("char")
        Character charWrapper();

        @WithName("boolean")
        boolean booleanPrimitive();

        @WithName("boolean")
        Boolean booleanWrapper();
    }

    interface Optionals {
        Optional<Server> server();

        Optional<String> optional();

        @WithName("optional.int")
        OptionalInt optionalInt();

        @WithParentName
        Map<String, Info> info();

        interface Info {
            String name();

            Optional<String> login();

            Optional<String> password();
        }
    }

    interface CollectionTypes {
        @WithName("strings")
        List<String> listStrings();

        @WithName("ints")
        List<Integer> listInts();
    }

    @ConfigMapping(prefix = "server")
    interface Maps {
        @WithParentName
        Map<String, String> server();

        Map<String, Server> group();

        @WithParentName
        Map<String, Server> groupParentName();
    }

    interface ComplexSample {
        ServerSub server();

        Optional<ServerSub> client();
    }

    interface Converters {
        @WithConverter(FooBarConverter.class)
        String foo();

        @WithConverter(BooleanConverter.class)
        boolean bprim();

        @WithConverter(CharacterConverter.class)
        char cprim();

        @WithConverter(IntegerConverter.class)
        int iprim();

        @WithConverter(ShortConverter.class)
        short sprim();

        @WithConverter(LongConverter.class)
        long lprim();

        @WithConverter(FloatConverter.class)
        float fprim();

        @WithConverter(DoubleConverter.class)
        double dprim();
    }

    public static class FooBarConverter implements Converter<String> {
        @Override
        public String convert(final String value) {
            return "bar";
        }
    }

    public static class BooleanConverter implements Converter<Boolean> {
        @Override
        public Boolean convert(String value) throws IllegalArgumentException, NullPointerException {
            return true;
        }
    }

    public static class CharacterConverter implements Converter<Character> {
        @Override
        public Character convert(String value) throws IllegalArgumentException, NullPointerException {
            return 'c';
        }
    }

    public static class IntegerConverter implements Converter<Integer> {
        @Override
        public Integer convert(String value) throws IllegalArgumentException, NullPointerException {
            return -1;
        }
    }

    public static class ShortConverter implements Converter<Short> {
        @Override
        public Short convert(String value) throws IllegalArgumentException, NullPointerException {
            return -1;
        }
    }

    public static class LongConverter implements Converter<Long> {
        @Override
        public Long convert(String value) throws IllegalArgumentException, NullPointerException {
            return -1L;
        }
    }

    public static class FloatConverter implements Converter<Float> {
        @Override
        public Float convert(String value) throws IllegalArgumentException, NullPointerException {
            return -1.0F;
        }
    }

    public static class DoubleConverter implements Converter<Double> {
        @Override
        public Double convert(String value) throws IllegalArgumentException, NullPointerException {
            return -1.0;
        }
    }

    @ConfigMapping(prefix = "cloud")
    interface ServerAnnotated {
        String host();

        int port();
    }

    interface ServerParent {
        String host();
    }

    interface ServerChild extends ServerParent {
        int port();
    }

    @ConfigMapping(prefix = "server")
    interface ServerConfigValue {
        ConfigValue host();

        ConfigValue port();
    }

    @ConfigMapping(prefix = "empty")
    interface Empty {

    }

    @ConfigMapping(prefix = "server")
    interface MapsInGroup {
        Info info();

        interface Info {
            String name();

            Map<String, String> values();

            Map<String, Data> data();
        }

        interface Data {
            String name();
        }
    }

    @Test
    void mapsInGroup() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config(
                        "server.info.name", "naruto",
                        "server.info.values.name", "naruto",
                        "server.info.data.first.name", "naruto"))
                .withMapping(MapsInGroup.class)
                .build();

        MapsInGroup mapping = config.getConfigMapping(MapsInGroup.class);
        assertNotNull(mapping);
        assertEquals("naruto", mapping.info().name());
        assertEquals("naruto", mapping.info().values().get("name"));
        assertEquals("naruto", mapping.info().data().get("first").name());
    }

    @ConfigMapping(prefix = "server")
    interface ServerPrefix {
        String host();

        int port();
    }

    @ConfigMapping(prefix = "server")
    interface ServerNamePrefix {
        String host();
    }

    @Test
    void prefixPropertyInRoot() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(configClass(ServerPrefix.class, "server"))
                .withMapping(configClass(ServerPrefix.class, "cloud.server"))
                .withMapping(configClass(ServerNamePrefix.class, "server"))
                .withSources(config("serverBoot", "server"))
                .withSources(config("server.host", "localhost", "server.port", "8080"))
                .withSources(config("cloud.serverBoot", "server"))
                .withSources(config("cloud.server.host", "localhost", "cloud.server.port", "8080"))
                .build();

        ServerPrefix server = config.getConfigMapping(ServerPrefix.class, "server");
        assertNotNull(server);
        assertEquals("localhost", server.host());
        assertEquals(8080, server.port());

        ServerPrefix serverCloud = config.getConfigMapping(ServerPrefix.class, "cloud.server");
        assertNotNull(server);
        assertEquals("localhost", serverCloud.host());
        assertEquals(8080, serverCloud.port());

        ServerNamePrefix serverName = config.getConfigMapping(ServerNamePrefix.class, "server");
        assertNotNull(server);
        assertEquals("localhost", serverName.host());
    }

    @Test
    void prefixPropertyInRootUnknown() {
        SmallRyeConfigBuilder builder = new SmallRyeConfigBuilder()
                .withMapping(ServerPrefix.class)
                .withSources(config("serverBoot", "server"))
                .withSources(config("server.host", "localhost", "server.port", "8080"))
                .withSources(config("server.name", "localhost"))
                .withSources(new EnvConfigSource(Map.of("SERVER_ALIAS", "alias"), 300));

        ConfigValidationException exception = assertThrows(ConfigValidationException.class, builder::build);
        assertEquals("SRCFG00050: server.name in KeyValuesConfigSource does not map to any root",
                exception.getProblem(0).getMessage());

        builder = new SmallRyeConfigBuilder()
                .withMapping(configClass(ServerPrefix.class, "server"))
                .withMapping(configClass(ServerPrefix.class, "cloud.server"))
                .withMapping(configClass(ServerNamePrefix.class, "server"))
                .withSources(config("serverBoot", "server"))
                .withSources(config("server.host", "localhost", "server.port", "8080"))
                .withSources(config("cloud.serverBoot", "server"))
                .withSources(config("cloud.server.host", "localhost", "cloud.server.port", "8080"))
                .withSources(config("cloud.server.name", "localhost"));

        exception = assertThrows(ConfigValidationException.class, builder::build);
        assertEquals("SRCFG00050: cloud.server.name in KeyValuesConfigSource does not map to any root",
                exception.getProblem(0).getMessage());
    }

    @ConfigMapping(prefix = "mapping.server.env")
    interface ServerMapEnv {
        @WithParentName
        Map<String, Info> info();

        String simpleDashedProperty();

        interface Info {
            String name();

            List<String> alias();
        }
    }

    @Test
    void mapEnv() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(new EnvConfigSource(new HashMap<>() {
                    {
                        put("MAPPING_SERVER_ENV__LOCALHOST__NAME", "development");
                        put("MAPPING_SERVER_ENV__LOCALHOST__ALIAS", "dev");
                        put("MAPPING_SERVER_ENV_CLOUD_NAME", "production");
                        put("MAPPING_SERVER_ENV_CLOUD_ALIAS", "prod");
                        put("MAPPING_SERVER_ENV_SIMPLE_DASHED_PROPERTY", "5678");
                        put("mapping.server.env.simple.dashed.property", "5678");
                    }
                }, 300))
                .withSources(config("mapping.server.env.simple-dashed-property", "1234"))
                .withMapping(ServerMapEnv.class)
                .build();

        ServerMapEnv mapping = config.getConfigMapping(ServerMapEnv.class);

        assertEquals("development", mapping.info().get("localhost").name());
        assertEquals("dev", mapping.info().get("localhost").alias().get(0));
        assertEquals("production", mapping.info().get("cloud").name());
        assertEquals("prod", mapping.info().get("cloud").alias().get(0));
        assertEquals("5678", mapping.simpleDashedProperty());
    }

    @ConfigMapping(prefix = "server")
    interface ServerOptionalWithName {
        @WithName("a_server")
        Optional<Server> aServer();

        interface Server {
            @WithName("a_host")
            String host();

            @WithName("a_port")
            int port();
        }
    }

    @Test
    void optionalWithName() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(new EnvConfigSource())
                .withMapping(ServerOptionalWithName.class)
                .withSources(config("server.a_server.a_host", "localhost", "server.a_server.a_port", "8080"))
                .build();

        ServerOptionalWithName mapping = config.getConfigMapping(ServerOptionalWithName.class);
        assertTrue(mapping.aServer().isPresent());
        assertEquals("localhost", mapping.aServer().get().host());
        assertEquals(8080, mapping.aServer().get().port());
    }

    @ConfigMapping(prefix = "server")
    interface ServerHierarchy extends Server {
        @WithParentName
        Named named();

        Optional<Alias> alias();

        interface Named {
            String name();
        }

        interface Alias extends Named {
            String alias();
        }
    }

    @Test
    void hierarchy() {
