/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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
 *
 *     https://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.commons.configuration2.interpol;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.function.Function;

import org.apache.commons.text.lookup.StringLookupFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * Test class for ConfigurationInterpolator.
 */
public class TestConfigurationInterpolator {

    /** Constant for a test variable name. */
    private static final String TEST_NAME = "varname";

    /** Constant for a test variable prefix. */
    private static final String TEST_PREFIX = "prefix";

    /** Constant for the value of the test variable. */
    private static final String TEST_VALUE = "TestVariableValue";

    private static void assertMappedLookups(final Map<String, Lookup> lookupMap, final String... keys) {
        final Set<String> remainingKeys = new HashSet<>(lookupMap.keySet());

        for (final String key : keys) {
            assertNotNull(key, "Expected map to contain string lookup for key " + key);

            remainingKeys.remove(key);
        }

        assertEquals(Collections.emptySet(), remainingKeys);
    }

    private static void checkDefaultPrefixLookupsHolder(final Properties props, final String... keys) {
        final ConfigurationInterpolator.DefaultPrefixLookupsHolder holder =
                new ConfigurationInterpolator.DefaultPrefixLookupsHolder(props);

        final Map<String, Lookup> lookupMap = holder.getDefaultPrefixLookups();

        assertMappedLookups(lookupMap, keys);
    }

    /**
     * Main method used to verify the default lookups resolved during JVM execution.
     * @param args
     */
    public static void main(final String[] args) {
        System.out.println("Default lookups");
        for (final String key : ConfigurationInterpolator.getDefaultPrefixLookups().keySet()) {
            System.out.println("- " + key);
        }
    }

    /**
     * Creates a lookup object that can resolve the test variable (and nothing else).
     *
     * @return the test lookup object
     */
    private static Lookup setUpTestLookup() {
        return setUpTestLookup(TEST_NAME, TEST_VALUE);
    }

    /**
     * Creates a lookup object that can resolve the specified variable (and nothing else).
     *
     * @param var the variable name
     * @param value the value of this variable
     * @return the test lookup object
     */
    private static Lookup setUpTestLookup(final String var, final Object value) {
        final Lookup lookup = mock(Lookup.class);
        when(lookup.lookup(any())).thenAnswer(invocation -> {
            if (var.equals(invocation.getArgument(0))) {
                return value;
            }
            return null;
        });
        return lookup;
    }

    /** Stores the object to be tested. */
    private ConfigurationInterpolator interpolator;

    @BeforeEach
    public void setUp() throws Exception {
        interpolator = new ConfigurationInterpolator();
    }

    /**
     * Tests whether multiple default lookups can be added.
     */
    @Test
    void testAddDefaultLookups() {
        final List<Lookup> lookups = new ArrayList<>();
        lookups.add(setUpTestLookup());
        lookups.add(setUpTestLookup("test", "value"));
        interpolator.addDefaultLookups(lookups);
        final List<Lookup> lookups2 = interpolator.getDefaultLookups();
        assertEquals(lookups, lookups2);
    }

    /**
     * Tests whether a null collection of default lookups is handled correctly.
     */
    @Test
    void testAddDefaultLookupsNull() {
        interpolator.addDefaultLookups(null);
        assertTrue(interpolator.getDefaultLookups().isEmpty());
    }

    @Test
    void testDefaultStringLookupsHolderAllLookups() {
        final Properties props = new Properties();
        props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY,
                "BASE64_DECODER BASE64_ENCODER const, date, dns, environment "
                + "file ,java, local_host properties, resource_bundle,script,system_properties "
                + "url url_decoder  , url_encoder, xml");

        checkDefaultPrefixLookupsHolder(props,
                "base64",
                StringLookupFactory.KEY_BASE64_DECODER,
                StringLookupFactory.KEY_BASE64_ENCODER,
                StringLookupFactory.KEY_CONST,
                StringLookupFactory.KEY_DATE,
                StringLookupFactory.KEY_ENV,
                StringLookupFactory.KEY_FILE,
                StringLookupFactory.KEY_JAVA,
                StringLookupFactory.KEY_LOCALHOST,
                StringLookupFactory.KEY_PROPERTIES,
                StringLookupFactory.KEY_RESOURCE_BUNDLE,
                StringLookupFactory.KEY_SYS,
                StringLookupFactory.KEY_URL_DECODER,
                StringLookupFactory.KEY_URL_ENCODER,
                StringLookupFactory.KEY_XML,

                StringLookupFactory.KEY_DNS,
                StringLookupFactory.KEY_URL,
                StringLookupFactory.KEY_SCRIPT);
    }

    @Test
    void testDefaultStringLookupsHolderGivenSingleLookup() {
        final Properties props = new Properties();
        props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "base64_encoder");
        checkDefaultPrefixLookupsHolder(props, "base64", StringLookupFactory.KEY_BASE64_ENCODER);
    }

    @Test
    void testDefaultStringLookupsHolderGivenSingleLookupWeirdString() {
        final Properties props = new Properties();
        props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, " \n \t  ,, DnS , , ");
        checkDefaultPrefixLookupsHolder(props, StringLookupFactory.KEY_DNS);
    }

    @Test
    void testDefaultStringLookupsHolderInvalidLookupsDefinition() {
        final Properties props = new Properties();
        props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "base64_encoder nope");
        final Exception exc = assertThrows(Exception.class, () -> new ConfigurationInterpolator.DefaultPrefixLookupsHolder(props));
        assertEquals("Invalid default lookups definition: base64_encoder nope", exc.getMessage());
    }

    @Test
    void testDefaultStringLookupsHolderLookupsPropertyEmptyAndBlank() {
        final Properties propsWithNull = new Properties();
        propsWithNull.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "");

        checkDefaultPrefixLookupsHolder(propsWithNull);

        final Properties propsWithBlank = new Properties();
        propsWithBlank.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, " ");

        checkDefaultPrefixLookupsHolder(propsWithBlank);
    }

    @Test
    void testDefaultStringLookupsHolderLookupsPropertyNotPresent() {
        checkDefaultPrefixLookupsHolder(new Properties(),
                "base64",
                StringLookupFactory.KEY_BASE64_DECODER,
                StringLookupFactory.KEY_BASE64_ENCODER,
                StringLookupFactory.KEY_CONST,
                StringLookupFactory.KEY_DATE,
                StringLookupFactory.KEY_ENV,
                StringLookupFactory.KEY_FILE,
                StringLookupFactory.KEY_JAVA,
                StringLookupFactory.KEY_LOCALHOST,
                StringLookupFactory.KEY_PROPERTIES,
                StringLookupFactory.KEY_RESOURCE_BUNDLE,
                StringLookupFactory.KEY_SYS,
                StringLookupFactory.KEY_URL_DECODER,
                StringLookupFactory.KEY_URL_ENCODER,
                StringLookupFactory.KEY_XML);
    }

    @Test
    void testDefaultStringLookupsHolderMultipleLookups() {
        final Properties props = new Properties();
        props.setProperty(ConfigurationInterpolator.DEFAULT_PREFIX_LOOKUPS_PROPERTY, "dns, url script ");

        checkDefaultPrefixLookupsHolder(props,
                StringLookupFactory.KEY_DNS,
                StringLookupFactory.KEY_URL,
                StringLookupFactory.KEY_SCRIPT);
    }

    /**
     * Tests deregistering a lookup object.
     */
    @Test
    void testDeregisterLookup() {
        final Lookup lookup = mock(Lookup.class);
        interpolator.registerLookup(TEST_PREFIX, lookup);
        assertTrue(interpolator.deregisterLookup(TEST_PREFIX));
        assertFalse(interpolator.prefixSet().contains(TEST_PREFIX));
        assertTrue(interpolator.getLookups().isEmpty());
    }

    /**
     * Tests deregistering an unknown lookup object.
     */
    @Test
    void testDeregisterLookupNonExisting() {
        assertFalse(interpolator.deregisterLookup(TEST_PREFIX));
    }

    /**
     * Tests whether the flag for substitution in variable names can be modified.
     */
    @Test
    void testEnableSubstitutionInVariables() {
        assertFalse(interpolator.isEnableSubstitutionInVariables());
        interpolator.addDefaultLookup(setUpTestLookup("java.version", "1.4"));
        interpolator.addDefaultLookup(setUpTestLookup("jre-1.4", "C:\\java\\1.4"));
        final String var = "${jre-${java.version}}";
        assertEquals(var, interpolator.interpolate(var));
        interpolator.setEnableSubstitutionInVariables(true);
        assertTrue(interpolator.isEnableSubstitutionInVariables());
        assertEquals("C:\\java\\1.4", interpolator.interpolate(var));
    }

    /**
     * Tests fromSpecification() if the specification contains an instance.
     */
    @Test
    void testFromSpecificationInterpolator() {
        final ConfigurationInterpolator ci = mock(ConfigurationInterpolator.class);
        final InterpolatorSpecification spec = new InterpolatorSpecification.Builder().withDefaultLookup(mock(Lookup.class))
            .withParentInterpolator(interpolator).withInterpolator(ci).create();
        assertSame(ci, ConfigurationInterpolator.fromSpecification(spec));
    }

    /**
     * Tests fromSpecification() if a new instance has to be created.
     */
    @Test
    void testFromSpecificationNewInstance() {
        final Lookup defLookup = mock(Lookup.class);
        final Lookup preLookup = mock(Lookup.class);
        final Function<Object, String> stringConverter = obj -> Objects.toString(obj, null);
        final InterpolatorSpecification spec = new InterpolatorSpecification.Builder()
            .withDefaultLookup(defLookup)
            .withPrefixLookup("p", preLookup)
            .withParentInterpolator(interpolator)
            .withStringConverter(stringConverter)
            .create();
        final ConfigurationInterpolator ci = ConfigurationInterpolator.fromSpecification(spec);
        assertEquals(Arrays.asList(defLookup), ci.getDefaultLookups());
        assertEquals(1, ci.getLookups().size());
        assertSame(preLookup, ci.getLookups().get("p"));
        assertSame(interpolator, ci.getParentInterpolator());
        assertSame(stringConverter, ci.getStringConverter());
    }

    /**
     * Tries to obtain an instance from a null specification.
     */
    @Test
    void testFromSpecificationNull() {
        assertThrows(IllegalArgumentException.class, () -> ConfigurationInterpolator.fromSpecification(null));
    }

    /**
     * Tests whether modification of the list of default lookups does not affect the object.
     */
    @Test
    void testGetDefaultLookupsModify() {
        final List<Lookup> lookups = interpolator.getDefaultLookups();
        lookups.add(setUpTestLookup());
        assertTrue(interpolator.getDefaultLookups().isEmpty());
    }

    /**
     * Tests whether default prefix lookups can be queried as a map.
     */
    @Test
    void testGetDefaultPrefixLookups() {
        final EnumSet<DefaultLookups> excluded = EnumSet.of(
                DefaultLookups.DNS,
                DefaultLookups.URL,
                DefaultLookups.SCRIPT);

        final EnumSet<DefaultLookups> included = EnumSet.complementOf(excluded);

        final Map<String, Lookup> lookups = ConfigurationInterpolator.getDefaultPrefixLookups();

        assertEquals(included.size(), lookups.size());
        for (final DefaultLookups l : included) {
            assertSame(l.getLookup(), lookups.get(l.getPrefix()), "Wrong entry for " + l);
        }

        for (final DefaultLookups l : excluded) {
            assertNull(lookups.get(l.getPrefix()), "Unexpected entry for " + l);
        }
    }

    /**
     * Tests that the map with default lookups cannot be modified.
     */
    @Test
    void testGetDefaultPrefixLookupsModify() {
        final Map<String, Lookup> lookups = ConfigurationInterpolator.getDefaultPrefixLookups();
        final Lookup lookup = mock(Lookup.class);
        assertThrows(UnsupportedOperationException.class, () -> lookups.put("test", lookup));

        verifyNoInteractions(lookup);
    }

    /**
     * Tests that modification of the map with lookups does not affect the object.
     */
    @Test
    void testGetLookupsModify() {
        final Map<String, Lookup> lookups = interpolator.getLookups();
        lookups.put(TEST_PREFIX, setUpTestLookup());
        assertTrue(interpolator.getLookups().isEmpty());
    }

    /**
     * Tests creating an instance. Does it contain some predefined lookups and a default string converter?
     */
    @Test
    void testInit() {
        assertTrue(interpolator.getDefaultLookups().isEmpty());
        assertTrue(interpolator.getLookups().isEmpty());
        assertNull(interpolator.getParentInterpolator());
        assertNotNull(interpolator.getStringConverter());
        assertEquals("1", interpolator.getStringConverter().apply(Arrays.asList(1, 2)));
    }

    /**
     * Tests interpolation of an array argument.
     */
    @Test
    void testInterpolateArray() {
        final int[] value = {1, 2};
        assertSame(value, interpolator.interpolate(value));
    }

    /**
     * Tests that a blank variable definition does not cause problems.
     */
    @Test
    void testInterpolateBlankVariable() {
        final String value = "${ }";
        assertEquals(value, interpolator.interpolate(value));
    }

    /**
     * Tests interpolation of a collection argument.
     */
    @Test
    void testInterpolateCollection() {
        final List<Integer> value = Arrays.asList(1, 2);
        assertSame(value, interpolator.interpolate(value));
    }

    /**
     * Tests that an empty variable definition does not cause problems.
     */
    @Test
    void testInterpolateEmptyVariable() {
        final String value = "${}";
        assertEquals(value, interpolator.interpolate(value));
    }

    /**
     * Tests interpolation of a non string argument.
     */
    @Test
    void testInterpolateObject() {
        final Object value = 42;
        assertSame(value, interpolator.interpolate(value));
    }

    /**
     * Tests a successful interpolation of a string value.
     */
    @Test
    void testInterpolateString() {
        final String value = "${" + TEST_PREFIX + ':' + TEST_NAME + "}";
        interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
        assertEquals(TEST_VALUE, interpolator.interpolate(value));
    }

    /**
     * Tests interpolation with a variable which cannot be resolved.
     */
    @Test
    void testInterpolateStringUnknownVariable() {
        final String value = "${unknownVariable}";
        assertEquals(value, interpolator.interpolate(value));
    }

    /**
     * Tests an interpolated string that begins and ends with variable lookups that have
     * the potential to fail. Part of CONFIGURATION-764.
     */
    @Test
    void testInterpolationBeginningAndEndingRiskyVariableLookups() {
        interpolator.registerLookups(ConfigurationInterpolator.getDefaultPrefixLookups());
        final String result = (String) interpolator.interpolate("${date:yyyy-MM}-${date:dd}");
        assertTrue(result.matches("\\d{4}-\\d{2}-\\d{2}"));
    }

    /**
     * Tests interpolation with multiple variables containing arrays.
     */
    @Test
    void testInterpolationMultipleArrayVariables() {
        final String value = "${single}bc${multi}23${empty}${null}";
        final int[] multi = {1, 0, 0};
        final String[] single = {"a"};
        final int[] empty = {};
        final Object[] containsNull = {null};
        interpolator.addDefaultLookup(setUpTestLookup("multi", multi));
        interpolator.addDefaultLookup(setUpTestLookup("single", single));
        interpolator.addDefaultLookup(setUpTestLookup("empty", empty));
        interpolator.addDefaultLookup(setUpTestLookup("null", containsNull));
        assertEquals("abc123${empty}${null}", interpolator.interpolate(value));
    }

    /**
     * Tests interpolation with multiple variables containing collections and iterators.
     */
    @Test
    void testInterpolationMultipleCollectionVariables() {
        final String value = "${single}bc${multi}23${empty}${null}${multiIt}${emptyIt}${nullIt}";
        final List<Integer> multi = Arrays.asList(1, 0, 0);
        final List<String> single = Arrays.asList("a");
        final List<Object> empty = Collections.emptyList();
        final List<Object> containsNull = Arrays.asList((Object) null);
        interpolator.addDefaultLookup(setUpTestLookup("multi", multi));
        interpolator.addDefaultLookup(setUpTestLookup("multiIt", multi.iterator()));
        interpolator.addDefaultLookup(setUpTestLookup("single", single));
        interpolator.addDefaultLookup(setUpTestLookup("empty", empty));
        interpolator.addDefaultLookup(setUpTestLookup("emptyIt", empty.iterator()));
        interpolator.addDefaultLookup(setUpTestLookup("null", containsNull));
        interpolator.addDefaultLookup(setUpTestLookup("nullIt", containsNull.iterator()));
        assertEquals("abc123${empty}${null}1${emptyIt}${nullIt}", interpolator.interpolate(value));
    }

    /**
     * Tests interpolation with variables containing multiple simple non-string variables.
     */
    @Test
    void testInterpolationMultipleSimpleNonStringVariables() {
        final String value = "${x} = ${y} is ${result}";
        interpolator.addDefaultLookup(setUpTestLookup("x", 1));
        interpolator.addDefaultLookup(setUpTestLookup("y", 2));
        interpolator.addDefaultLookup(setUpTestLookup("result", false));
        assertEquals("1 = 2 is false", interpolator.interpolate(value));
    }

    /**
     * Tests a property value consisting of multiple variables.
     */
    @Test
    void testInterpolationMultipleVariables() {
        final String value = "The ${subject} jumps over ${object}.";
        interpolator.addDefaultLookup(setUpTestLookup("subject", "quick brown fox"));
        interpolator.addDefaultLookup(setUpTestLookup("object", "the lazy dog"));
        assertEquals("The quick brown fox jumps over the lazy dog.", interpolator.interpolate(value));
    }

    /**
     * Tests an interpolation that consists of a single array variable only. The variable's value
     * should be returned verbatim.
     */
    @Test
    void testInterpolationSingleArrayVariable() {
        final int[] value = {42, -1};
        interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
        assertEquals(value, interpolator.interpolate("${" + TEST_NAME + "}"));
    }

    /**
     * Tests an interpolation that consists of a single collection variable only. The variable's value
     * should be returned verbatim.
     */
    @Test
    void testInterpolationSingleCollectionVariable() {
        final List<Integer> value = Arrays.asList(42);
        interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
        assertEquals(value, interpolator.interpolate("${" + TEST_NAME + "}"));
    }

    /**
     * Tests an interpolation that consists of a single variable only. The variable's value should be returned verbatim.
     */
    @Test
    void testInterpolationSingleVariable() {
        final Object value = 42;
        interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
        assertEquals(value, interpolator.interpolate("${" + TEST_NAME + "}"));
    }

    /**
     * Tests an interpolation that consists of a single undefined variable only with and without a default value.
     */
    @Test
    void testInterpolationSingleVariableDefaultValue() {
        final Object value = 42;
        interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, value));
        assertEquals("${I_am_not_defined}", interpolator.interpolate("${I_am_not_defined}"));
        assertEquals("42", interpolator.interpolate("${I_am_not_defined:-42}"));
        assertEquals("", interpolator.interpolate("${I_am_not_defined:-}"));
    }

    /**
     * Tests a variable declaration which lacks the trailing closing bracket.
     */
    @Test
    void testInterpolationVariableIncomplete() {
        final String value = "${" + TEST_NAME;
        interpolator.addDefaultLookup(setUpTestLookup(TEST_NAME, "someValue"));
        assertEquals(value, interpolator.interpolate(value));
    }

    /**
     * Tests nullSafeLookup() if a lookup object was provided.
     */
    @Test
    void testNullSafeLookupExisting() {
        final Lookup look = mock(Lookup.class);
        assertSame(look, ConfigurationInterpolator.nullSafeLookup(look));
    }

    /**
     * Tests whether nullSafeLookup() can handle null input.
     */
    @Test
    void testNullSafeLookupNull() {
        final Lookup lookup = ConfigurationInterpolator.nullSafeLookup(null);
        assertNull(lookup.lookup("someVar"));
    }

    /**
     * Tests that the prefix set cannot be modified.
     */
    @Test
    void testPrefixSetModify() {
        interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
        final Iterator<String> it = interpolator.prefixSet().iterator();
        it.next();
        assertThrows(UnsupportedOperationException.class, it::remove);
    }

    /**
     * Tests registering a lookup object at an instance.
     */
    @Test
    void testRegisterLookup() {
        final Lookup lookup = mock(Lookup.class);
        interpolator.registerLookup(TEST_PREFIX, lookup);
        assertSame(lookup, interpolator.getLookups().get(TEST_PREFIX));
        assertTrue(interpolator.prefixSet().contains(TEST_PREFIX));
        assertTrue(interpolator.getDefaultLookups().isEmpty());
    }

    /**
     * Tests registering a null lookup object. This should cause an exception.
     */
    @Test
    void testRegisterLookupNull() {
        assertThrows(IllegalArgumentException.class, () -> interpolator.registerLookup(TEST_PREFIX, null));
    }

    /**
     * Tests registering a lookup object for an undefined prefix. This should cause an exception.
     */
    @Test
    void testRegisterLookupNullPrefix() {
        final Lookup lookup = mock(Lookup.class);
        assertThrows(IllegalArgumentException.class, () -> interpolator.registerLookup(null, lookup));

        verifyNoInteractions(lookup);
    }

    /**
     * Tests whether a map with lookup objects can be registered.
     */
    @Test
    void testRegisterLookups() {
        final Lookup l1 = setUpTestLookup();
        final Lookup l2 = setUpTestLookup("someVar", "someValue");
        final Map<String, Lookup> lookups = new HashMap<>();
        lookups.put(TEST_PREFIX, l1);
        final String prefix2 = TEST_PREFIX + "_other";
        lookups.put(prefix2, l2);
        interpolator.registerLookups(lookups);
        final Map<String, Lookup> lookups2 = interpolator.getLookups();

        final Map<String, Lookup> expected = new HashMap<>();
        expected.put(TEST_PREFIX, l1);
        expected.put(prefix2, l2);
        assertEquals(expected, lookups2);
    }

    /**
     * Tests whether a null map with lookup objects is handled correctly.
     */
    @Test
    void testRegisterLookupsNull() {
        interpolator.registerLookups(null);
        assertTrue(interpolator.getLookups().isEmpty());
    }

    /**
     * Tests whether a default lookup object can be removed.
     */
    @Test
    void testRemoveDefaultLookup() {
        final List<Lookup> lookups = new ArrayList<>();
        lookups.add(setUpTestLookup());
        lookups.add(setUpTestLookup("test", "value"));
        interpolator.addDefaultLookups(lookups);
        assertTrue(interpolator.removeDefaultLookup(lookups.get(0)));
        assertFalse(interpolator.getDefaultLookups().contains(lookups.get(0)));
        assertEquals(1, interpolator.getDefaultLookups().size());
    }

    /**
     * Tests whether a non existing default lookup object can be removed.
     */
    @Test
    void testRemoveDefaultLookupNonExisting() {
        assertFalse(interpolator.removeDefaultLookup(setUpTestLookup()));
    }

    /**
     * Tests looking up a variable without a prefix. This should trigger the default lookup object.
     */
    @Test
    void testResolveDefault() {
        final Lookup l1 = mock(Lookup.class);
        final Lookup l2 = mock(Lookup.class);
        final Lookup l3 = mock(Lookup.class);

        when(l1.lookup(TEST_NAME)).thenReturn(null);
        when(l2.lookup(TEST_NAME)).thenReturn(TEST_VALUE);

        interpolator.addDefaultLookups(Arrays.asList(l1, l2, l3));
        assertEquals(TEST_VALUE, interpolator.resolve(TEST_NAME));

        verify(l1).lookup(TEST_NAME);
        verify(l2).lookup(TEST_NAME);
        verifyNoMoreInteractions(l1, l2, l3);
    }

    /**
     * Tests whether the default lookup is called for variables with a prefix when the lookup that was registered for this
     * prefix is not able to resolve the variable.
     */
    @Test
    void testResolveDefaultAfterPrefixFails() {
        final String varName = TEST_PREFIX + ':' + TEST_NAME + "2";
        interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
        interpolator.addDefaultLookup(setUpTestLookup(varName, TEST_VALUE));
        assertEquals(TEST_VALUE, interpolator.resolve(varName));
    }

    /**
     * Tests an empty variable name without a prefix.
     */
    @Test
    void testResolveDefaultEmptyVarName() {
        interpolator.addDefaultLookup(setUpTestLookup("", TEST_VALUE));
        assertEquals(TEST_VALUE, interpolator.resolve(""));
    }

    /**
     * Tests the empty variable prefix. This is a special case, but legal.
     */
    @Test
    void testResolveEmptyPrefix() {
        interpolator.registerLookup("", setUpTestLookup());
        assertEquals(TEST_VALUE, interpolator.resolve(":" + TEST_NAME));
    }

    /**
     * Tests an empty variable name.
     */
    @Test
    void testResolveEmptyVarName() {
        interpolator.registerLookup(TEST_PREFIX, setUpTestLookup("", TEST_VALUE));
        assertEquals(TEST_VALUE, interpolator.resolve(TEST_PREFIX + ":"));
    }

    /**
     * Tests looking up a variable without a prefix when no default lookup is specified. Result should be null in this case.
     */
    @Test
    void testResolveNoDefault() {
        assertNull(interpolator.resolve(TEST_NAME));
    }

    /**
     * Tests looking up a null variable. Result should be null, too.
     */
    @Test
    void testResolveNull() {
        assertNull(interpolator.resolve(null));
    }

    /**
     * Tests handling of a parent {@code ConfigurationInterpolator} if the variable can already be resolved by the current
     * instance.
     */
    @Test
    void testResolveParentVariableFound() {
        final ConfigurationInterpolator parent = mock(ConfigurationInterpolator.class);
        interpolator.setParentInterpolator(parent);
        interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
        assertEquals(TEST_VALUE, interpolator.resolve(TEST_PREFIX + ':' + TEST_NAME));
    }

    /**
     * Tests whether the parent {@code ConfigurationInterpolator} is invoked if the test instance cannot resolve a variable.
     */
    @Test
    void testResolveParentVariableNotFound() {
        final ConfigurationInterpolator parent = mock(ConfigurationInterpolator.class);

        when(parent.resolve(TEST_NAME)).thenReturn(TEST_VALUE);

        interpolator.setParentInterpolator(parent);
        assertEquals(TEST_VALUE, interpolator.resolve(TEST_NAME));

        verify(parent).resolve(TEST_NAME);
        verifyNoMoreInteractions(parent);
    }

    /**
     * Tests whether a variable can be resolved using the associated lookup object. The lookup is identified by the
     * variable's prefix.
     */
    @Test
    void testResolveWithPrefix() {
        interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
        assertEquals(TEST_VALUE, interpolator.resolve(TEST_PREFIX + ':' + TEST_NAME));
    }

    /**
     * Tests the behavior of the lookup method for variables with an unknown prefix. These variables should not be resolved.
     */
    @Test
    void testResolveWithUnknownPrefix() {
        interpolator.registerLookup(TEST_PREFIX, setUpTestLookup());
        assertNull(interpolator.resolve("UnknownPrefix:" + TEST_NAME));
        assertNull(interpolator.resolve(":" + TEST_NAME));
    }

    /**
     * Tests that a custom string converter can be used.
     */
    @Test
    void testSetStringConverter() {
        final Function<Object, String> stringConverter = obj -> "'" + obj + "'";
        interpolator.addDefaultLookup(setUpTestLookup("x", Arrays.asList(1, 2)));
        interpolator.addDefaultLookup(setUpTestLookup("y", "abc"));
        interpolator.setStringConverter(stringConverter);
        assertSame(stringConverter, interpolator.getStringConverter());
        assertEquals("'abc': '[1, 2]'", interpolator.interpolate("${y}: ${x}"));
    }

    /**
     * Tests that the default string converter can be reapplied by passing {@code null}.
     */
    @Test
    void testSetStringConverterNullArgumentUsesDefault() {
        final Function<Object, String> stringConverter = obj -> "'" + obj + "'";
        interpolator.addDefaultLookup(setUpTestLookup("x", Arrays.asList(1, 2)));
        interpolator.addDefaultLookup(setUpTestLookup("y", "abc"));
        interpolator.setStringConverter(stringConverter);
        interpolator.setStringConverter(null);
        assertNotSame(stringConverter, interpolator.getStringConverter());
        assertEquals("abc: 1", interpolator.interpolate("${y}: ${x}"));
    }
}
