/**
 * 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.camel.core.xml;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

import static java.util.Arrays.asList;
import static java.util.Arrays.stream;

import org.apache.camel.Service;
import org.apache.camel.TypeConverter;
import org.apache.camel.impl.DefaultClassResolver;
import org.apache.camel.impl.DefaultFactoryFinder;
import org.apache.camel.impl.DefaultPackageScanClassResolver;
import org.apache.camel.impl.converter.DefaultTypeConverter;
import org.apache.camel.model.ModelCamelContext;
import org.apache.camel.spi.ExecutorServiceManager;
import org.apache.camel.spi.ManagementNameStrategy;
import org.apache.camel.spi.RuntimeEndpointRegistry;
import org.apache.camel.util.ReflectionInjector;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.Invocation;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

public class AbstractCamelContextFactoryBeanTest {

    // any properties (abstract methods in AbstractCamelContextFactoryBean that
    // return String and receive no arguments) that do not support property
    // placeholders
    Set<String> propertiesThatAreNotPlaceholdered = Collections.singleton("{{getErrorHandlerRef}}");

    TypeConverter typeConverter = new DefaultTypeConverter(new DefaultPackageScanClassResolver(),
        new ReflectionInjector(),
        new DefaultFactoryFinder(new DefaultClassResolver(), "META-INF/services/org/apache/camel/"), false);

    // properties that should return value that can be converted to boolean
    Set<String> valuesThatReturnBoolean = new HashSet<>(asList("{{getStreamCache}}", "{{getTrace}}",
        "{{getMessageHistory}}", "{{getLogMask}}", "{{getLogExhaustedMessageBody}}", "{{getHandleFault}}",
        "{{getAutoStartup}}", "{{getUseMDCLogging}}", "{{getUseDataType}}", "{{getUseBreadcrumb}}", "{{getAllowUseOriginalMessage}}"));

    // properties that should return value that can be converted to long
    Set<String> valuesThatReturnLong = new HashSet<>(asList("{{getDelayer}}"));

    public AbstractCamelContextFactoryBeanTest() throws Exception {
        ((Service) typeConverter).start();
    }

    @Test
    public void shouldSupportPropertyPlaceholdersOnAllProperties() throws Exception {
        final Set<Invocation> invocations = new LinkedHashSet<>();

        final ModelCamelContext context = mock(ModelCamelContext.class,
            withSettings().invocationListeners(i -> invocations.add((Invocation) i.getInvocation())));

        // program the property resolution in context mock
        when(context.resolvePropertyPlaceholders(anyString())).thenAnswer(invocation -> {
            final String placeholder = invocation.getArgument(0);

            // we receive the argument and check if the method should return a
            // value that can be converted to boolean
            if (valuesThatReturnBoolean.contains(placeholder) || placeholder.endsWith("Enabled}}")) {
                return "true";
            }

            // or long
            if (valuesThatReturnLong.contains(placeholder)) {
                return "1";
            }

            // else is just plain string
            return "string";
        });
        when(context.getTypeConverter()).thenReturn(typeConverter);
        when(context.getRuntimeEndpointRegistry()).thenReturn(mock(RuntimeEndpointRegistry.class));
        when(context.getManagementNameStrategy()).thenReturn(mock(ManagementNameStrategy.class));
        when(context.getExecutorServiceManager()).thenReturn(mock(ExecutorServiceManager.class));

        @SuppressWarnings("unchecked")
        final AbstractCamelContextFactoryBean<ModelCamelContext> factory = mock(AbstractCamelContextFactoryBean.class);
        when(factory.getContext()).thenReturn(context);
        doCallRealMethod().when(factory).initCamelContext(context);

        final Set<String> expectedPropertiesToBeResolved = propertiesToBeResolved(factory);

        // method under test
        factory.initCamelContext(context);

        // we want to capture the arguments initCamelContext tried to resolve
        // and check if it tried to resolve all placeholders we expected
        final ArgumentCaptor<String> capturedPlaceholders = ArgumentCaptor.forClass(String.class);
        verify(context, atLeastOnce()).resolvePropertyPlaceholders(capturedPlaceholders.capture());

        // removes any properties that are not using property placeholders
        expectedPropertiesToBeResolved.removeAll(propertiesThatAreNotPlaceholdered);

        assertThat(capturedPlaceholders.getAllValues())
            .as("The expectation is that all abstract getter methods that return Strings should support property "
                + "placeholders, and that for those will delegate to CamelContext::resolvePropertyPlaceholders, "
                + "we captured all placeholders that tried to resolve and found differences")
            .containsAll(expectedPropertiesToBeResolved);
    }

    Set<String> propertiesToBeResolved(final AbstractCamelContextFactoryBean<ModelCamelContext> factory) {
        final Set<String> expectedPropertiesToBeResolved = new HashSet<>();

        // looks at all abstract methods in AbstractCamelContextFactoryBean that
        // do have no declared parameters and programs the mock to return
        // "{{methodName}}" on calling that method, this happens when
        // AbstractCamelContextFactoryBean::initContext invokes the programmed
        // mock, so the returned collection will be empty until initContext
        // invokes the mocked method
        stream(AbstractCamelContextFactoryBean.class.getDeclaredMethods())
            .filter(m -> Modifier.isAbstract(m.getModifiers()) && m.getParameterCount() == 0).forEach(m -> {
                try {
                    when(m.invoke(factory)).thenAnswer(invocation -> {
                        final Method method = invocation.getMethod();

                        final String name = method.getName();

                        if (String.class.equals(method.getReturnType())) {
                            final String placeholder = "{{" + name + "}}";
                            expectedPropertiesToBeResolved.add(placeholder);
                            return placeholder;
                        }

                        return null;
                    });
                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ignored) {
                    // ignored
                }
            });

        return expectedPropertiesToBeResolved;
    }

    static boolean shouldProvidePropertyPlaceholderSupport(final Method method) {
        // all abstract getter methods that return String are possibly returning
        // strings that contain property placeholders

        final boolean isAbstract = Modifier.isAbstract(method.getModifiers());
        final boolean isGetter = method.getName().startsWith("get");
        final Class<?> returnType = method.getReturnType();

        final boolean isCompatibleReturnType = String.class.isAssignableFrom(returnType);

        return isAbstract && isGetter && isCompatibleReturnType;
    }

}
