package io.smallrye.config;

import static io.smallrye.config.ConfigMappings.registerConfigMappings;
import static io.smallrye.config.ConfigMappings.registerConfigProperties;
import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix;
import static io.smallrye.config.KeyValuesConfigSource.config;
import static java.util.Collections.singleton;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

import io.smallrye.config.ConfigMappingInterface.Property;

public class ConfigMappingsTest {
    @Test
    void registerMapping() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("server.host", "localhost", "server.port", "8080",
                        "server.reasons.200", "OK", "server.reasons.201", "Created",
                        "server.versions.v1", "1.The version 1.2.3",
                        "server.versions.v2", "2.The version 2.0.0",
                        "server.numbers.one", "1", "server.numbers.two", "2", "server.numbers.three", "3"))
                .withConverter(Version.class, 100, new VersionConverter())
                .build();

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

        assertEquals("localhost", server.host());
        assertEquals(8080, server.port());
        assertEquals(2, server.reasons().size());
        assertEquals("OK", server.reasons().get(200));
        assertEquals("Created", server.reasons().get(201));
        assertEquals(2, server.versions().size());
        assertEquals(new Version(1, "The version 1.2.3"), server.versions().get("v1"));
        assertEquals(new Version(2, "The version 2.0.0"), server.versions().get("v2"));
        assertEquals(3, server.numbers().size());
        assertEquals(1, server.numbers().get("one"));
        assertEquals(2, server.numbers().get("two"));
        assertEquals(3, server.numbers().get("three"));
    }

    @Test
    void registerProperties() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("server.host", "localhost", "server.port", "8080",
                        "server.reasons.200", "OK", "server.reasons.201", "Created",
                        "server.versions.v1", "1.The version 1.2.3",
                        "server.versions.v2", "2.The version 2.0.0",
                        "server.numbers.one", "1", "server.numbers.two", "2", "server.numbers.three", "3"))
                .withConverter(Version.class, 100, new VersionConverter())
                .build();

        registerConfigProperties(config, singleton(configClassWithPrefix(ServerClass.class, "server")));
        ServerClass server = config.getConfigMapping(ServerClass.class);

        assertEquals("localhost", server.host);
        assertEquals(8080, server.port);
        assertEquals(2, server.reasons.size());
        assertEquals("OK", server.reasons.get(200));
        assertEquals("Created", server.reasons.get(201));
        assertEquals(2, server.versions.size());
        assertEquals(new Version(1, "The version 1.2.3"), server.versions.get("v1"));
        assertEquals(new Version(2, "The version 2.0.0"), server.versions.get("v2"));
        assertEquals(3, server.numbers.size());
        assertEquals(1, server.numbers.get("one"));
        assertEquals(2, server.numbers.get("two"));
        assertEquals(3, server.numbers.get("three"));

        Map<String, Version> versions = config.getValues("server.versions", String.class, Version.class);
        assertEquals(2, versions.size());
        assertEquals(new Version(1, "The version 1.2.3"), versions.get("v1"));
        assertEquals(new Version(2, "The version 2.0.0"), versions.get("v2"));

        Optional<Map<String, Version>> versionsOptional = config.getOptionalValues("optional.versions", String.class,
                Version.class);
        assertFalse(versionsOptional.isPresent());
    }

    @Test
    void validate() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("server.host", "localhost", "server.port", "8080", "server.unmapped", "unmapped"))
                .build();

        assertThrows(ConfigValidationException.class,
                () -> registerConfigMappings(config, singleton(configClassWithPrefix(Server.class, "server"))),
                "server.unmapped does not map to any root");

        registerConfigProperties(config, singleton(configClassWithPrefix(ServerClass.class, "server")));
        ServerClass server = config.getConfigMapping(ServerClass.class);

        assertEquals("localhost", server.host);
        assertEquals(8080, server.port);
    }

    @Test
    void validateWithBuilderOrConfig() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withSources(config("server.host", "localhost", "server.port", "8080", "server.unmapped", "unmapped"))
                .withMapping(ServerClass.class, "server")
                .withValidateUnknown(true)
                .withDefaultValue(SmallRyeConfig.SMALLRYE_CONFIG_MAPPING_VALIDATE_UNKNOWN, "false")
                .build();

        ServerClass server = config.getConfigMapping(ServerClass.class);

        assertEquals("localhost", server.host);
        assertEquals(8080, server.port);
    }

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

        ServerClass server = config.getConfigMapping(ServerClass.class);

        assertEquals("localhost", server.host);
        assertEquals(8080, server.port);
    }

    @Test
    void validateAnnotations() {
        SmallRyeConfig config = new SmallRyeConfigBuilder().build();
        IllegalStateException exception = assertThrows(IllegalStateException.class,
                () -> registerConfigMappings(config, singleton(configClassWithPrefix(ServerMappingClass.class, "server"))));
        assertTrue(exception.getMessage()
                .startsWith("SRCFG00043: The @ConfigMapping annotation can only be placed in interfaces"));

        exception = assertThrows(IllegalStateException.class,
                () -> new SmallRyeConfigBuilder().withMapping(ServerMappingClass.class, "server").build());
        assertTrue(exception.getMessage()
                .startsWith("SRCFG00043: The @ConfigMapping annotation can only be placed in interfaces"));

        exception = assertThrows(IllegalStateException.class,
                () -> registerConfigMappings(config,
                        singleton(configClassWithPrefix(ServerPropertiesInterface.class, "server"))));
        assertTrue(exception.getMessage()
                .startsWith("SRCFG00044: The @ConfigProperties annotation can only be placed in classes"));

        exception = assertThrows(IllegalStateException.class,
                () -> new SmallRyeConfigBuilder().withMapping(ServerPropertiesInterface.class, "server").build());
        assertTrue(exception.getMessage()
                .startsWith("SRCFG00044: The @ConfigProperties annotation can only be placed in classes"));
    }

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

        int port();

        List<Alias> aliases();

        String toString();

        interface Alias {
            String name();

            String toString();
        }
    }

    @Test
    void generateToString() {
        SmallRyeConfig config = new SmallRyeConfigBuilder()
                .withMapping(ToStringMapping.class)
                .withSources(config(
                        "server.host", "localhost",
                        "server.port", "8080",
                        "server.aliases[0].name", "prod-1",
                        "server.aliases[1].name", "prod-2"))
                .withConverter(Version.class, 100, new VersionConverter())
                .build();

        ToStringMapping mapping = config.getConfigMapping(ToStringMapping.class);
        assertEquals("ToStringMapping{host=localhost, port=8080, aliases=[Alias{name=prod-1}, Alias{name=prod-2}]}",
                mapping.toString());
    }

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

        int port();

        Map<Integer, String> reasons();

        Map<String, Version> versions();

        Map<String, Integer> numbers();
    }

    @ConfigProperties(prefix = "server")
    static class ServerClass {
        String host;
        int port;
        Map<Integer, String> reasons;
        Map<String, Version> versions;
        Map<String, Integer> numbers;
    }

    @ConfigMapping(prefix = "server")
    static class ServerMappingClass {
        String host;
    }

    @ConfigProperties(prefix = "server")
    interface ServerPropertiesInterface {
        String host();
    }

    static class Version {
        int id;
        String name;

        Version(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;
            Version version = (Version) o;
            return id == version.id && Objects.equals(name, version.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, name);
        }
    }

    static class VersionConverter implements Converter<Version> {
        @Override
        public Version convert(String value) {
            return new Version(Integer.parseInt(value.substring(0, 1)), value.substring(2));
        }
    }

    @ConfigMapping(prefix = "mapped")
    interface MappedProperties {
        String value();

        Nested nested();

        List<Nested> collection();

        interface Nested {
            String value();
        }
    }

    @Test
    void properties() {
        Map<String, Property> properties = ConfigMappings.getProperties(configClassWithPrefix(MappedProperties.class));
        assertEquals(3, properties.size());
    }

    @Test
    void mappedProperties() {
        Set<String> mappedProperties = mappedProperties(MappedProperties.class, "mapped.value", "mapped.nested.value",
                "mapped.collection[0].value", "mapped.unknown");
        assertEquals(3, mappedProperties.size());
        assertTrue(mappedProperties.contains("mapped.value"));
        assertTrue(mappedProperties.contains("mapped.nested.value"));
        assertTrue(mappedProperties.contains("mapped.collection[0].value"));
    }

    private static Set<String> mappedProperties(final Class<?> mappingClass, final String... properties) {
        return ConfigMappings.mappedProperties(configClassWithPrefix(mappingClass),
                Stream.of(properties).collect(Collectors.toSet()));
    }
}
