package org.apache.maven.surefire.testng.conf;

/*
 * 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.
 */

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;
        }
    }
}
