/*
 * 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.maven.surefire.testng.conf;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.maven.surefire.api.booter.ProviderParameterNames;
import org.apache.maven.surefire.api.testset.TestSetFailedException;
import org.testng.TestNG;
import org.testng.xml.XmlSuite;

/**
 * Configurator that relies on reflection to set parameters in TestNG
 *
 */
public abstract class AbstractDirectConfigurator implements Configurator {
    final Map<String, Setter> setters;

    AbstractDirectConfigurator() {
        Map<String, Setter> options = new HashMap<>();
        // options.put( ProviderParameterNames.TESTNG_GROUPS_PROP, new Setter( "setGroups", String.class ) );
        // options.put( ProviderParameterNames.TESTNG_EXCLUDEDGROUPS_PROP, new Setter( "setExcludedGroups", String.class
        // ) );
        options.put("junit", new Setter("setJUnit", Boolean.class));
        options.put(ProviderParameterNames.THREADCOUNT_PROP, new Setter("setThreadCount", int.class));
        options.put("usedefaultlisteners", new Setter("setUseDefaultListeners", boolean.class));
        this.setters = options;
    }

    @Override
    public void configure(TestNG testng, Map<String, String> options) throws TestSetFailedException {
        System.out.println("\n\n\n\nCONFIGURING TESTNG\n\n\n\n");
        // kind of ugly, but listeners are configured differently
        final String listeners = options.remove("listener");
        // DGF In 4.7, default listeners dump XML files in the surefire-reports directory,
        // confusing the report plugin.  This was fixed in later versions.
        testng.setUseDefaultListeners(false);
        configureInstance(testng, options);
        // TODO: we should have the Profile so that we can decide if this is needed or not
        testng.setListenerClasses(loadListenerClasses(listeners));
    }

    @Override
    public void configure(XmlSuite suite, Map<String, String> options) throws TestSetFailedException {
        Map<String, String> filtered = filterForSuite(options);
        configureInstance(suite, filtered);
    }

    protected Map<String, String> filterForSuite(Map<String, String> options) {
        Map<String, String> result = new HashMap<>();
        addPropIfNotNull(options, result, ProviderParameterNames.PARALLEL_PROP);
        addPropIfNotNull(options, result, ProviderParameterNames.THREADCOUNT_PROP);
        return result;
    }

    private void addPropIfNotNull(Map<String, String> options, Map<String, String> result, String prop) {
        if (options.containsKey(prop)) {
            result.put(prop, options.get(prop));
        }
    }

    private void configureInstance(Object testngInstance, Map<String, String> options) {
        for (Map.Entry<String, String> entry : options.entrySet()) {
            String key = entry.getKey();
            String val = entry.getValue();
            Setter setter = setters.get(key);
            if (setter != null) {
                try {
                    setter.invoke(testngInstance, val);
                } catch (Exception e) {
                    throw new RuntimeException("Cannot set option " + key + " with value " + val, e);
                }
            }
        }
    }

    static List<Class> loadListenerClasses(String listenerClasses) throws TestSetFailedException {
        if (listenerClasses == null || listenerClasses.trim().isEmpty()) {
            return new ArrayList<>();
        }

        List<Class> classes = new ArrayList<>();
        String[] classNames = listenerClasses.split("\\s*,\\s*(\\r?\\n)?\\s*");
        for (String className : classNames) {
            Class<?> clazz = loadClass(className);
            classes.add(clazz);
        }

        return classes;
    }

    static Class<?> loadClass(String className) throws TestSetFailedException {
        try {
            return Class.forName(className);
        } catch (Exception ex) {
            throw new TestSetFailedException("Cannot find listener class " + className, ex);
        }
    }

    /**
     * Describes a property setter by method name and parameter class
     *
     */
    public static final class Setter {
        private final String setterName;

        private final Class<?> paramClass;

        public Setter(String name, Class<?> clazz) {
            setterName = name;
            paramClass = clazz;
        }

        public void invoke(Object target, String value) throws Exception {
            Method setter = target.getClass().getMethod(setterName, paramClass);
            if (setter != null) {
                setter.invoke(target, convertValue(value));
            }
        }

        private Object convertValue(String value) {
            if (value == null) {
                return null;
            }
            if (paramClass.isAssignableFrom(value.getClass())) {
                return value;
            }

            if (Boolean.class.equals(paramClass) || boolean.class.equals(paramClass)) {
                return Boolean.valueOf(value);
            }
            if (Integer.class.equals(paramClass) || int.class.equals(paramClass)) {
                return Integer.valueOf(value);
            }

            return value;
        }
    }
}
