/*
 * 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
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.bookkeeper.common.conf;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.google.common.collect.Lists;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Function;
import org.apache.commons.configuration2.CompositeConfiguration;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;

/**
 * Unit test {@link ConfigKey}.
 */
public class ConfigKeyTest {

    /**
     * Test Function A.
     */
    private static class TestFunctionA implements Function<String, String> {

        @Override
        public String apply(String s) {
            return s + "!";
        }
    }

    /**
     * Test Function B.
     */
    private static class TestFunctionB implements Function<String, String> {

        @Override
        public String apply(String s) {
            return s + "!";
        }
    }

    /**
     * Test Function C.
     */
    private static class TestFunctionC implements Function<String, String> {

        @Override
        public String apply(String s) {
            return s + "!";
        }
    }

    @Rule
    public final TestName runtime = new TestName();

    @Test
    public void testValidateRequiredField() {
        String keyName = runtime.getMethodName();
        Configuration conf = new ConcurrentConfiguration();
        ConfigKey key = ConfigKey.builder(keyName)
            .required(true)
            .build();

        try {
            key.validate(conf);
            fail("Required key should exist in the configuration");
        } catch (ConfigException ce) {
            // expected
        }
    }

    @Test
    public void testValidateFieldSuccess() throws ConfigException {
        String keyName = runtime.getMethodName();
        Validator validator = mock(Validator.class);
        when(validator.validate(anyString(), any())).thenReturn(true);
        Configuration conf = new ConcurrentConfiguration();
        conf.setProperty(keyName, "test-value");
        ConfigKey key = ConfigKey.builder(keyName)
            .validator(validator)
            .build();

        key.validate(conf);
        verify(validator, times(1)).validate(eq(keyName), eq("test-value"));
    }

    @Test
    public void testValidateFieldFailure() {
        String keyName = runtime.getMethodName();
        Validator validator = mock(Validator.class);
        when(validator.validate(anyString(), any())).thenReturn(false);
        Configuration conf = new ConcurrentConfiguration();
        conf.setProperty(keyName, "test-value");
        ConfigKey key = ConfigKey.builder(keyName)
            .validator(validator)
            .build();

        try {
            key.validate(conf);
            fail("Should fail validation if validator#validate returns false");
        } catch (ConfigException ce) {
            // expected
        }
        verify(validator, times(1)).validate(eq(keyName), eq("test-value"));
    }

    @Test
    public void testGetLong() {
        String keyName = runtime.getMethodName();
        long defaultValue = System.currentTimeMillis();
        ConfigKey key = ConfigKey.builder(keyName)
            .required(true)
            .type(Type.LONG)
            .defaultValue(defaultValue)
            .build();

        Configuration conf = new ConcurrentConfiguration();

        // get default value
        assertEquals(defaultValue, key.getLong(conf));
        assertEquals(defaultValue, key.get(conf));

        // set value
        long newValue = System.currentTimeMillis() * 2;
        key.set(conf, newValue);
        assertEquals(newValue, key.getLong(conf));
        assertEquals(newValue, key.get(conf));
    }

    @Test
    public void testGetInt() {
        String keyName = runtime.getMethodName();
        int defaultValue = ThreadLocalRandom.current().nextInt(10000);
        ConfigKey key = ConfigKey.builder(keyName)
            .required(true)
            .type(Type.INT)
            .defaultValue(defaultValue)
            .build();

        Configuration conf = new ConcurrentConfiguration();

        // get default value
        assertEquals(defaultValue, key.getInt(conf));
        assertEquals(defaultValue, key.get(conf));

        // set value
        int newValue = defaultValue * 2;
        key.set(conf, newValue);
        assertEquals(newValue, key.getInt(conf));
        assertEquals(newValue, key.get(conf));
    }

    @Test
    public void testGetShort() {
        String keyName = runtime.getMethodName();
        short defaultValue = (short) ThreadLocalRandom.current().nextInt(10000);
        ConfigKey key = ConfigKey.builder(keyName)
            .required(true)
            .type(Type.SHORT)
            .defaultValue(defaultValue)
            .build();

        Configuration conf = new ConcurrentConfiguration();

        // get default value
        assertEquals(defaultValue, key.getShort(conf));
        assertEquals(defaultValue, key.get(conf));

        // set value
        short newValue = (short) (defaultValue * 2);
        key.set(conf, newValue);
        assertEquals(newValue, key.getShort(conf));
        assertEquals(newValue, key.get(conf));
    }

    @Test
    public void testGetDouble() {
        String keyName = runtime.getMethodName();
        double defaultValue = ThreadLocalRandom.current().nextDouble(10000.0f);
        ConfigKey key = ConfigKey.builder(keyName)
            .required(true)
            .type(Type.DOUBLE)
            .defaultValue(defaultValue)
            .build();

        Configuration conf = new ConcurrentConfiguration();

        // get default value
        assertEquals(defaultValue, key.getDouble(conf), 0.0001);
        assertEquals(defaultValue, key.get(conf));

        // set value
        double newValue = (defaultValue * 2);
        key.set(conf, newValue);
        assertEquals(newValue, key.getDouble(conf), 0.0001);
        assertEquals(newValue, key.get(conf));
    }

    @Test
    public void testGetBoolean() {
        String keyName = runtime.getMethodName();
        boolean defaultValue = ThreadLocalRandom.current().nextBoolean();
        ConfigKey key = ConfigKey.builder(keyName)
            .required(true)
            .type(Type.BOOLEAN)
            .defaultValue(defaultValue)
            .build();

        Configuration conf = new ConcurrentConfiguration();

        // get default value
        assertEquals(defaultValue, key.getBoolean(conf));
        assertEquals(defaultValue, key.get(conf));

        // set value
        boolean newValue = !defaultValue;
        key.set(conf, newValue);
        assertEquals(newValue, key.getBoolean(conf));
        assertEquals(newValue, key.get(conf));
    }

    @Test
    public void testGetList() {
        String keyName = runtime.getMethodName();
        List<String> defaultList = Lists.newArrayList(
            "item1", "item2", "item3"
        );
        ConfigKey key = ConfigKey.builder(keyName)
            .required(true)
            .type(Type.LIST)
            .defaultValue(defaultList)
            .build();

        Configuration conf = new CompositeConfiguration() {
            {
                setListDelimiterHandler(new DefaultListDelimiterHandler(','));
            }
        };

        // get default value
        assertEquals(defaultList, key.getList(conf));
        assertEquals(defaultList, key.get(conf));

        // set value
        List<String> newList = Lists.newArrayList(
            "item4", "item5", "item6"
        );
        key.set(conf, newList);
        assertEquals(newList, key.getList(conf));
        assertEquals(newList, key.get(conf));

        // set string value
        newList = Lists.newArrayList(
            "item7", "item8", "item9"
        );
        conf.setProperty(key.name(), "item7,item8,item9");
        assertEquals(newList, key.getList(conf));
        assertEquals(newList, key.get(conf));
    }

    @Test
    public void testGetClass() {
        String keyName = runtime.getMethodName();
        Class defaultClass = TestFunctionA.class;
        ConfigKey key = ConfigKey.builder(keyName)
            .required(true)
            .type(Type.CLASS)
            .defaultValue(defaultClass)
            .build();

        Configuration conf = new CompositeConfiguration();

        // get default value
        assertEquals(defaultClass, key.getClass(conf));
        assertEquals(defaultClass, key.get(conf));

        // set value
        Class newClass = TestFunctionB.class;
        key.set(conf, newClass);
        assertEquals(newClass, key.getClass(conf));
        assertEquals(newClass, key.get(conf));

        // set string value
        String newClassName = TestFunctionC.class.getName();
        conf.setProperty(key.name(), newClassName);
        assertEquals(TestFunctionC.class, key.getClass(conf));
        assertEquals(TestFunctionC.class, key.get(conf));
    }

    @Test
    public void testGetString() {
        String keyName = runtime.getMethodName();
        String defaultValue = "default-string-value";
        ConfigKey key = ConfigKey.builder(keyName)
            .required(true)
            .type(Type.STRING)
            .defaultValue(defaultValue)
            .build();

        Configuration conf = new CompositeConfiguration();

        // get default value
        assertEquals(defaultValue, key.getString(conf));
        assertEquals(defaultValue, key.get(conf));

        // set value
        String newValue = "new-string-value";
        key.set(conf, newValue);
        assertEquals(newValue, key.getString(conf));
        assertEquals(newValue, key.get(conf));
    }

}
