/*
 * 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.brooklyn.util.core.internal;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.objs.Configurable;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
import org.apache.brooklyn.core.config.BasicConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.flags.FlagUtils;
import org.apache.brooklyn.util.core.flags.FlagUtils.FlagConfigKeyAndValueRecord;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.guava.Maybe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.Test;

import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;

public class FlagUtilsTest {

    public static final Logger log = LoggerFactory.getLogger(FlagUtilsTest.class);
    
    @Test
    public void testGetAllFields() {
        log.info("types {}", FlagUtils.getAllAssignableTypes(Baz.class));
        assertEquals(FlagUtils.getAllAssignableTypes(Baz.class), ImmutableList.of(Baz.class, Foo.class, Bar.class));
        List<Field> fs = FlagUtils.getAllFields(Baz.class);
        for (Field f : fs) {
            log.info("field {}    {}", f.getName(), f);
        }
        List<String> fsn = ImmutableList.copyOf(Iterables.transform(fs, new Function<Field, String>() {
            @Override public String apply(Field f) {
                return f.getName();
            }}));
        assertTrue(fsn.indexOf("A") >= 0);
        assertTrue(fsn.indexOf("w") > fsn.indexOf("A")); 
        assertTrue(fsn.indexOf("x") > fsn.indexOf("A") );
        assertTrue(fsn.indexOf("yNotY") > fsn.indexOf("A")); 
        assertTrue(fsn.indexOf("Z") > fsn.indexOf("yNotY") );
    }    
    
    @Test
    public void testSetFieldsFromFlags() {
        Foo f = new Foo();
        Map<?,?> m = MutableMap.of("w", 3, "x", 1, "y", 7, "z", 9);
        Map<?, ?> unused = FlagUtils.setFieldsFromFlags(m, f);
        assertEquals(f.w, 3);
        assertEquals(f.x, 1);
        assertEquals(f.yNotY, 7);
        assertEquals(unused, ImmutableMap.of("z", 9));
        Map<?,?> m2 = FlagUtils.getFieldsWithValues(f);
        m.remove("z");
        assertEquals(m2, m);
    }
    
    @Test
    public void testCollectionCoercionOnSetFromFlags() {
        WithSpecialFieldTypes s = new WithSpecialFieldTypes();
        Map<?,?> m = ImmutableMap.of("set", ImmutableSet.of(1));
        FlagUtils.setFieldsFromFlags(m, s);
        assertEquals(s.set, ImmutableSet.of(1));
    }

    @Test
    public void testInetAddressCoercionOnSetFromFlags() {
        WithSpecialFieldTypes s = new WithSpecialFieldTypes();
        Map<?,?> m = ImmutableMap.of("inet", "127.0.0.1");
        FlagUtils.setFieldsFromFlags(m, s);
        assertEquals(s.inet.getHostAddress(), "127.0.0.1");
    }

    @Test
    public void testNonImmutableField() {
        Foo f = new Foo();
        FlagUtils.setFieldsFromFlags(ImmutableMap.of("w", 8), f);
        assertEquals(f.w, 8);
        FlagUtils.setFieldsFromFlags(ImmutableMap.of("w", 9), f);
        assertEquals(f.w, 9);
    }

    @Test
    public void testImmutableIntField() {
        Foo f = new Foo();
        FlagUtils.setFieldsFromFlags(ImmutableMap.of("x", 8), f);
        assertEquals(f.x, 8);
        boolean succeededWhenShouldntHave = false; 
        try {
            FlagUtils.setFieldsFromFlags(ImmutableMap.of("x", 9), f);
            succeededWhenShouldntHave = true;
        } catch (IllegalStateException e) {
            //expected
        }
        assertFalse(succeededWhenShouldntHave);
        assertEquals(f.x, 8);
    }

    @Test
    public void testImmutableObjectField() {
        WithImmutableNonNullableObject o = new WithImmutableNonNullableObject();
        FlagUtils.setFieldsFromFlags(ImmutableMap.of("a", "a", "b", "b"), o);
        assertEquals(o.a, "a");
        assertEquals(o.b, "b");
        
        FlagUtils.setFieldsFromFlags(ImmutableMap.of("a", "a2"), o);
        assertEquals(o.a, "a2");
        
        boolean succeededWhenShouldntHave = false;
        try {
            FlagUtils.setFieldsFromFlags(ImmutableMap.of("b", "b2"), o);
            succeededWhenShouldntHave = true;
        } catch (IllegalStateException e) {
            //expected
        }
        assertFalse(succeededWhenShouldntHave);
        assertEquals(o.b, "b");
    }

    @Test
    public void testNonNullable() {
        WithImmutableNonNullableObject o = new WithImmutableNonNullableObject();
        //allowed
        FlagUtils.setFieldsFromFlags(MutableMap.of("a", null), o);
        assertEquals(o.a, null);
        assertEquals(o.b, null);
        //not allowed
        boolean succeededWhenShouldntHave = false;
        try {
            FlagUtils.setFieldsFromFlags(MutableMap.of("b", null), o);
            succeededWhenShouldntHave = true;
        } catch (IllegalArgumentException e) {
            //expected
        }
        assertFalse(succeededWhenShouldntHave);
        assertEquals(o.b, null);
    }
    
    @Test
    public void testGetAnnotatedFields() throws Exception {
        Map<Field, SetFromFlag> fm = FlagUtils.getAnnotatedFields(WithImmutableNonNullableObject.class);
        assertEquals(fm.keySet().size(), 2);
        assertTrue(fm.get(WithImmutableNonNullableObject.class.getDeclaredField("b")).immutable());
    }

    @Test
    public void testCheckRequired() {
        WithImmutableNonNullableObject f = new WithImmutableNonNullableObject();
        FlagUtils.setFieldsFromFlags(ImmutableMap.of("a", "a is a"), f);
        assertEquals(f.a, "a is a");
        assertEquals(f.b, null);
        int exceptions = 0;
        try {
            FlagUtils.checkRequiredFields(f);
        } catch (IllegalStateException e) {
            exceptions++;
        }
        assertEquals(exceptions, 1);
    }

    @Test
    public void testSetConfigKeys() {
        FooCK f = new FooCK();
        Map<?,?> unused = FlagUtils.setFieldsFromFlags(ImmutableMap.of("f1", 9, "ck1", "do-set", "ck2", "dont-set", "c3", "do-set"), f);
        assertEquals(f.bag.get(FooCK.CK1), "do-set");
        assertEquals(f.bag.get(FooCK.CK3), "do-set");
        assertEquals(f.f1, 9);
        assertEquals(f.bag.containsKey(FooCK.CK2), false);
        assertEquals(unused, ImmutableMap.of("ck2", "dont-set"));
    }
    
    @Test
    public void testSetAllConfigKeys() {
        FooCK f = new FooCK();
        Map<?,?> unused = FlagUtils.setAllConfigKeys(ImmutableMap.of("f1", 9, "ck1", "do-set", "ck2", "do-set-2", "c3", "do-set"), f, true);
        assertEquals(f.bag.get(FooCK.CK1), "do-set");
        assertEquals(f.bag.get(FooCK.CK3), "do-set");
        assertEquals(f.bag.containsKey(FooCK.CK2), true);
        assertEquals(f.bag.get(FooCK.CK2), "do-set-2");
        assertEquals(unused, ImmutableMap.of("f1", 9));
    }

    @Test
    public void testSetFromConfigKeys() {
        FooCK f = new FooCK();
        Map<?, ?> unused = FlagUtils.setFieldsFromFlags(ImmutableMap.of(new BasicConfigKey<Integer>(Integer.class, "f1"), 9, "ck1", "do-set", "ck2", "dont-set"), f);
        assertEquals(f.bag.get(FooCK.CK1), "do-set");
        assertEquals(f.f1, 9);
        assertEquals(f.bag.containsKey(FooCK.CK2), false);
        assertEquals(unused, ImmutableMap.of("ck2", "dont-set"));
    }

    @Test
    public void testFindAllConfigKeys() {
        Map<String, ConfigKey<?>> keys = FlagUtils.findAllConfigKeys(null, ImmutableList.of(SubFooCK.class));
        assertEquals(keys, ImmutableMap.of(
                FooCK.CK1.getName(), FooCK.CK1, 
                FooCK.CK2.getName(), FooCK.CK2, 
                FooCK.CK3.getName(), FooCK.CK3,
                SubFooCK.CK4.getName(), SubFooCK.CK4));
    }

    @Test
    public void testFindAllFlagsAndConfigKeysWithEmptyBag() {
        ConfigBag bag = ConfigBag.newInstance(ImmutableMap.of());
        List<FlagConfigKeyAndValueRecord> vals = FlagUtils.<SubFooCK>findAllFlagsAndConfigKeys(null, SubFooCK.class, bag);
        assertRecordsEqual(vals, ImmutableList.<ExpectedFlagConfigKeyAndValueRecord>of());
    }

    @Test
    public void testFindAllFlagsAndConfigKeysWithConfigVals() {
        ConfigBag bag = ConfigBag.newInstance(ImmutableMap.of(FooCK.CK1, "ck1.myval"));
        List<FlagConfigKeyAndValueRecord> vals = FlagUtils.<SubFooCK>findAllFlagsAndConfigKeys(null, SubFooCK.class, bag);
        assertRecordsEqual(vals, ImmutableList.of(new ExpectedFlagConfigKeyAndValueRecord(FooCK.CK1, "CK1", "ck1.myval")));
    }

    @Test
    public void testFindAllFlagsAndConfigKeysWithFlagVals() {
        ConfigBag bag = ConfigBag.newInstance(ImmutableMap.of("f1", 123));
        List<FlagConfigKeyAndValueRecord> vals = FlagUtils.<SubFooCK>findAllFlagsAndConfigKeys(null, SubFooCK.class, bag);
        assertRecordsEqual(vals, ImmutableList.of(new ExpectedFlagConfigKeyAndValueRecord("f1", 123)));
    }

    protected void assertRecordsEqual(List<FlagConfigKeyAndValueRecord> actual, List<ExpectedFlagConfigKeyAndValueRecord> expected) {
        List<FlagConfigKeyAndValueRecord> actualCopy = MutableList.copyOf(actual);
        for (ExpectedFlagConfigKeyAndValueRecord record : expected) {
            boolean found = false;
            for (FlagConfigKeyAndValueRecord contender : actualCopy) {
                if (isRecordEqual(contender, record)) {
                    actualCopy.remove(contender);
                    found = true;
                    break;
                }
            }
            if (!found) {
                fail(record+" expected, but not found");
            }
        }
        if (!actualCopy.isEmpty()) {
            fail(actualCopy+" additional records found");
        }
    }

    protected boolean isRecordEqual(FlagConfigKeyAndValueRecord val1, ExpectedFlagConfigKeyAndValueRecord val2) {
        return Objects.equal(val1.getConfigKey(), val2.configKey)
                && Objects.equal(val1.getFlagName(), val2.flagName)
                && Objects.equal(val1.getConfigKeyMaybeValue(), val2.configKeyValue)
                && Objects.equal(val1.getFlagMaybeValue(), val2.flagValue);
    }

    // The fields of FlagConfigKeyAndValueRecord are private; hence having this class to make assertions easier
    static class ExpectedFlagConfigKeyAndValueRecord {
        String flagName = "";
        ConfigKey<?> configKey = null;
        Maybe<Object> flagValue = Maybe.absent();
        Maybe<Object> configKeyValue = Maybe.absent();
        
        ExpectedFlagConfigKeyAndValueRecord(String flagName, Object val) {
            this.flagName = flagName;
            this.flagValue = Maybe.of(val);
        }
        ExpectedFlagConfigKeyAndValueRecord(ConfigKey<?> key, String flagName, Object val) {
            this.configKey = key;
            this.flagName = flagName;
            this.configKeyValue = Maybe.of(val);
        }
        @Override
        public String toString() {
            return Objects.toStringHelper(this).omitNullValues()
                    .add("flag", flagName)
                    .add("configKey", configKey)
                    .add("flagValue", flagValue.orNull())
                    .add("configKeyValue", configKeyValue.orNull())
                    .toString();
        }
    }
    
    public static class Foo {
        @SetFromFlag
        int w;
        
        @SetFromFlag(immutable=true)
        private int x;
        
        @SetFromFlag("y")
        public int yNotY;
    }
    
    public static interface Bar {
        static final String Z = "myzval";
    }
    
    public static class Baz extends Foo implements Bar {
        @SuppressWarnings("unused")  //inspected by reflection
        private static int A;
    }
    
    public static class WithImmutableNonNullableObject {
        @SetFromFlag
        Object a;
        @SetFromFlag(immutable=true, nullable=false)
        public Object b;
    }
    
    public static class WithSpecialFieldTypes {
        @SetFromFlag Set<?> set;
        @SetFromFlag InetAddress inet;
    }

    public static class FooCK implements Configurable {
        @SetFromFlag
        public static ConfigKey<String> CK1 = ConfigKeys.newStringConfigKey("ck1");
        
        public static ConfigKey<String> CK2 = ConfigKeys.newStringConfigKey("ck2");

        @SetFromFlag("c3")
        public static ConfigKey<String> CK3 = ConfigKeys.newStringConfigKey("ck3");

        @SetFromFlag
        int f1;
        
        ConfigBag bag = new ConfigBag();
        BasicConfigurationSupport configSupport = new BasicConfigurationSupport();
        
        @Override
        public ConfigurationSupport config() {
            return configSupport;
        }
        
        @Override
        public <T> T getConfig(ConfigKey<T> key) {
            return config().get(key);
        }
        
        @Override
        public <T> T setConfig(ConfigKey<T> key, T val) {
            return config().set(key, val);
        }
        
        private class BasicConfigurationSupport implements ConfigurationSupport {
            @Override
            public <T> T get(ConfigKey<T> key) {
                return bag.get(key);
            }

            @Override
            public <T> T get(HasConfigKey<T> key) {
                return get(key.getConfigKey());
            }

            @Override
            public <T> T set(ConfigKey<T> key, T val) {
                T old = bag.get(key);
                bag.configure(key, val);
                return old;
            }

            @Override
            public <T> T set(HasConfigKey<T> key, T val) {
                return set(key.getConfigKey(), val);
            }

            @Override
            public <T> T set(ConfigKey<T> key, Task<T> val) {
                throw new UnsupportedOperationException();
            }

            @Override
            public <T> T set(HasConfigKey<T> key, Task<T> val) {
                return set(key.getConfigKey(), val);
            }

            @Override @Deprecated
            public Set<ConfigKey<?>> findKeys(Predicate<? super ConfigKey<?>> filter) {
            	return findKeysDeclared(filter);
            }
            
            @Override
            public Set<ConfigKey<?>> findKeysDeclared(Predicate<? super ConfigKey<?>> filter) {
            	return MutableSet.copyOf(Iterables.filter(bag.getAllConfigAsConfigKeyMap().keySet(), filter));
            }
            
            @Override
            public Set<ConfigKey<?>> findKeysPresent(Predicate<? super ConfigKey<?>> filter) {
            	return findKeysDeclared(filter);
            }

        }
    }
    
    public static class SubFooCK extends FooCK {
        public static ConfigKey<String> CK4 = ConfigKeys.newStringConfigKey("ck4");
    }
}