/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.common.inject;

import org.elasticsearch.common.inject.spi.Element;
import org.elasticsearch.common.inject.spi.Elements;
import org.elasticsearch.common.inject.spi.InstanceBinding;
import org.elasticsearch.common.inject.spi.LinkedKeyBinding;
import org.elasticsearch.common.inject.spi.ProviderInstanceBinding;
import org.elasticsearch.common.inject.spi.ProviderLookup;
import org.elasticsearch.common.inject.spi.UntargettedBinding;
import org.elasticsearch.test.ESTestCase;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

/**
 * Base testcase for testing {@link Module} implementations.
 */
public abstract class ModuleTestCase extends ESTestCase {

    /** Configures the module and asserts "clazz" is bound to "to". */
    public void assertBinding(Module module, Class to, Class clazz) {
        List<Element> elements = Elements.getElements(module);
        for (Element element : elements) {
            if (element instanceof LinkedKeyBinding) {
                LinkedKeyBinding binding = (LinkedKeyBinding) element;
                if (to.equals(binding.getKey().getTypeLiteral().getType())) {
                    assertSame(clazz, binding.getLinkedKey().getTypeLiteral().getType());
                    return;
                }
            } else if (element instanceof UntargettedBinding) {
                UntargettedBinding binding = (UntargettedBinding) element;
                if (to.equals(binding.getKey().getTypeLiteral().getType())) {
                    assertSame(clazz, to);
                    return;
                }
            }
        }
        StringBuilder s = new StringBuilder();
        for (Element element : elements) {
            s.append(element + "\n");
        }
        fail("Did not find any binding to " + to.getName() + ". Found these bindings:\n" + s);
    }

    /** Configures the module and asserts "clazz" is not bound to anything. */
    public void assertNotBound(Module module, Class clazz) {
        List<Element> elements = Elements.getElements(module);
        for (Element element : elements) {
            if (element instanceof LinkedKeyBinding) {
                LinkedKeyBinding binding = (LinkedKeyBinding) element;
                if (clazz.equals(binding.getKey().getTypeLiteral().getType())) {
                    fail("Found binding for " + clazz.getName() + " to " + binding.getKey().getTypeLiteral().getType().getTypeName());
                }
            } else if (element instanceof UntargettedBinding) {
                UntargettedBinding binding = (UntargettedBinding) element;
                if (clazz.equals(binding.getKey().getTypeLiteral().getType())) {
                    fail("Found binding for " + clazz.getName());
                }
            }
        }
    }

    /**
     * Attempts to configure the module, and asserts an {@link IllegalArgumentException} is
     * caught, containing the given messages
     */
    public void assertBindingFailure(Module module, String... msgs) {
        try {
            List<Element> elements = Elements.getElements(module);
            StringBuilder s = new StringBuilder();
            for (Element element : elements) {
                s.append(element + "\n");
            }
            fail("Expected exception from configuring module. Found these bindings:\n" + s);
        } catch (IllegalArgumentException e) {
            for (String msg : msgs) {
                assertTrue(e.getMessage() + " didn't contain: " + msg, e.getMessage().contains(msg));
            }
        }
    }

    /**
     * Configures the module and checks a Map&lt;String, Class&gt; of the "to" class
     * is bound to "theClass".
     */
    public void assertMapMultiBinding(Module module, Class to, Class theClass) {
        List<Element> elements = Elements.getElements(module);
        Set<Type> bindings = new HashSet<>();
        boolean providerFound = false;
        for (Element element : elements) {
            if (element instanceof LinkedKeyBinding) {
                LinkedKeyBinding binding = (LinkedKeyBinding) element;
                if (to.equals(binding.getKey().getTypeLiteral().getType())) {
                    bindings.add(binding.getLinkedKey().getTypeLiteral().getType());
                }
            } else if (element instanceof ProviderInstanceBinding) {
                ProviderInstanceBinding binding = (ProviderInstanceBinding) element;
                String setType = binding.getKey().getTypeLiteral().getType().toString();
                if (setType.equals("java.util.Map<java.lang.String, " + to.getName() + ">")) {
                    providerFound = true;
                }
            }
        }

        if (bindings.contains(theClass) == false) {
            fail("Expected to find " + theClass.getName() + " as binding to " + to.getName() + ", found these classes:\n" + bindings);
        }
        assertTrue("Did not find provider for map of " + to.getName(), providerFound);
    }


    /**
     * Configures the module and checks a Set of the "to" class
     * is bound to "classes". There may be more classes bound
     * to "to" than just "classes".
     */
    public void assertSetMultiBinding(Module module, Class to, Class... classes) {
        List<Element> elements = Elements.getElements(module);
        Set<Type> bindings = new HashSet<>();
        boolean providerFound = false;
        for (Element element : elements) {
            if (element instanceof LinkedKeyBinding) {
                LinkedKeyBinding binding = (LinkedKeyBinding) element;
                if (to.equals(binding.getKey().getTypeLiteral().getType())) {
                    bindings.add(binding.getLinkedKey().getTypeLiteral().getType());
                }
            } else if (element instanceof ProviderInstanceBinding) {
                ProviderInstanceBinding binding = (ProviderInstanceBinding) element;
                String setType = binding.getKey().getTypeLiteral().getType().toString();
                if (setType.equals("java.util.Set<" + to.getName() + ">")) {
                    providerFound = true;
                }
            }
        }

        for (Class clazz : classes) {
            if (bindings.contains(clazz) == false) {
                fail("Expected to find " + clazz.getName() + " as set binding to " + to.getName() + ", found these classes:\n" + bindings);
            }
        }
        assertTrue("Did not find provider for set of " + to.getName(), providerFound);
    }

    /**
     * Configures the module, and ensures an instance is bound to the "to" class, and the
     * provided tester returns true on the instance.
     */
    public <T> void assertInstanceBinding(Module module, Class<T> to, Predicate<T> tester) {
        assertInstanceBindingWithAnnotation(module, to, tester, null);
    }

    /**
     * Configures the module, and returns an instance  bound to the "to" class.
     */
    public static <T> T bindAndGetInstance(Module module, Class<T> to) {
        List<Element> elements = Elements.getElements(module);
        for (Element element : elements) {
            if (element instanceof InstanceBinding) {
                InstanceBinding binding = (InstanceBinding) element;
                if (to.equals(binding.getKey().getTypeLiteral().getType())) {
                    return to.cast(binding.getInstance());
                }
            } else  if (element instanceof ProviderInstanceBinding) {
                ProviderInstanceBinding binding = (ProviderInstanceBinding) element;
                if (to.equals(binding.getKey().getTypeLiteral().getType())) {
                    return to.cast(binding.getProviderInstance().get());
                }
            }
        }
        fail("can't get instance for class " + to);
        return null; // won't happen ;)
    }

    /**
     * Like {@link #assertInstanceBinding(Module, Class, Predicate)}, but filters the
     * classes checked by the given annotation.
     */
    public <T> void assertInstanceBindingWithAnnotation(Module module, Class<T> to, Predicate<T> tester, Class<? extends Annotation> annotation) {
        List<Element> elements = Elements.getElements(module);
        for (Element element : elements) {
            if (element instanceof InstanceBinding) {
                InstanceBinding binding = (InstanceBinding) element;
                if (to.equals(binding.getKey().getTypeLiteral().getType())) {
                    if (annotation == null || annotation.equals(binding.getKey().getAnnotationType())) {
                        assertTrue(tester.test(to.cast(binding.getInstance())));
                        return;
                    }
                }
            } else  if (element instanceof ProviderInstanceBinding) {
                ProviderInstanceBinding binding = (ProviderInstanceBinding) element;
                if (to.equals(binding.getKey().getTypeLiteral().getType())) {
                    assertTrue(tester.test(to.cast(binding.getProviderInstance().get())));
                    return;
                }
            }
        }
        StringBuilder s = new StringBuilder();
        for (Element element : elements) {
            s.append(element + "\n");
        }
        fail("Did not find any instance binding to " + to.getName() + ". Found these bindings:\n" + s);
    }

    /**
     * Configures the module, and ensures a map exists between the "keyType" and "valueType",
     * and that all of the "expected" values are bound.
     */
    @SuppressWarnings("unchecked")
    public <K, V> void assertMapInstanceBinding(Module module, Class<K> keyType, Class<V> valueType, Map<K, V> expected) throws Exception {
        // this method is insane because java type erasure makes it incredibly difficult...
        Map<K, Key> keys = new HashMap<>();
        Map<Key, V> values = new HashMap<>();
        List<Element> elements = Elements.getElements(module);
        for (Element element : elements) {
            if (element instanceof InstanceBinding) {
                InstanceBinding binding = (InstanceBinding) element;
                if (binding.getKey().getRawType().equals(valueType)) {
                    values.put(binding.getKey(), (V) binding.getInstance());
                } else if (binding.getInstance() instanceof Map.Entry) {
                    Map.Entry entry = (Map.Entry) binding.getInstance();
                    Object key = entry.getKey();
                    Object providerValue = entry.getValue();
                    if (key.getClass().equals(keyType) && providerValue instanceof ProviderLookup.ProviderImpl) {
                        ProviderLookup.ProviderImpl provider = (ProviderLookup.ProviderImpl) providerValue;
                        keys.put((K) key, provider.getKey());
                    }
                }
            }
        }
        for (Map.Entry<K, V> entry : expected.entrySet()) {
            Key valueKey = keys.get(entry.getKey());
            assertNotNull("Could not find binding for key [" + entry.getKey() + "], found these keys:\n" + keys.keySet(), valueKey);
            V value = values.get(valueKey);
            assertNotNull("Could not find value for instance key [" + valueKey + "], found these bindings:\n" + elements);
            assertEquals(entry.getValue(), value);
        }
    }
}
