/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira.junit
 */

package net.ranides.assira.junit;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import javax.annotation.Resource;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
@SuppressWarnings("PMD.MethodNameRules")
final class TestUtils {
    
    // @todo (assira.junit) wywalić type inference Function na podst constant pool
    // nie mamy 100% gwarancji, że rozpozna dobrze
    // a w frameworku do unit testów to oznacza, że MUSI wylecieć

	private TestUtils() { 
		/* utility class */
	}

	public static Object copy(Object object) {
		try {
			return object.getClass().newInstance();
		} catch (ReflectiveOperationException cause) {
			return rethrow(cause);
		}
	}
	
	public static Class<?> typeof(Method method) {
        Class<?> cparam = method.getParameterTypes()[0];
        if(Supplier.class.isAssignableFrom(cparam)) {
            return typeof_supplier(method);
        }
        if(Function.class.isAssignableFrom(cparam)) {
            return typeof_function(method);
        }
        return cparam;
    }
	
	private static Class<?> typeof_supplier(Method method) throws AssertionError {
        ParameterizedType gparam = (ParameterizedType)method.getGenericParameterTypes()[0];
        Type tparam = gparam.getActualTypeArguments()[0];
        if(tparam instanceof Class<?>) {
            return (Class<?>)tparam;
        }
        if(tparam instanceof ParameterizedType) {
            return (Class<?>)(((ParameterizedType)tparam).getRawType());
        }
        throw new AssertionError("You can't use wildcard generic types in method: " + method, cause4method(method, "Invalid declaration"));
    }
    
    private static Class<?> typeof_function(Method method) throws AssertionError {
        ParameterizedType gparam = (ParameterizedType)method.getGenericParameterTypes()[0];

        Type tparam = gparam.getActualTypeArguments()[1];
        if(tparam instanceof Class<?>) {
            return (Class<?>)tparam;
        }
        if(tparam instanceof ParameterizedType) {
            return (Class<?>)(((ParameterizedType)tparam).getRawType());
        }
        throw new AssertionError("You can't use wildcard generic types in method: " + method, cause4method(method, "Invalid declaration"));
    }
	
	public static Exception cause4method(Method method, String message) {
        Exception e = new Exception(message); // NOPMD
        e.setStackTrace(new StackTraceElement[]{
            new StackTraceElement(
                method.getDeclaringClass().getName(),
                method.getName(), "?", -1)
        });
        return e;
    }
	
	public static boolean acceptsSupplier(Method method) {
		return method.getParameterTypes()[0].equals(Supplier.class);
	}
	
	public static boolean acceptsFunction(Method method) {
		return method.getParameterTypes()[0].equals(Function.class);
	}
	
	public static String uid(Method method) {
		return TestUtils.typeof(method).getName() + "#" + method.getDeclaringClass().getName() + "#" + method.getName();
	}
	
	public static String nid(Method method) {
		return method.getDeclaringClass().getSimpleName() + "." + method.getName();
	}
	
	public static List<IField> getFields(Object object) {
		List<IField> ret = new ArrayList<>();
		for(Class<?> c=object.getClass(); c!=null; c=c.getSuperclass()) {
			for(Field f : c.getDeclaredFields()) {
				Resource info = f.getAnnotation(Resource.class);
				if(null != info) {
					String name = info.name();
					if("".equals(name)) {
						name = f.getName();
					}
					f.setAccessible(true);
					ret.add(new IField(object, f, name));
				}
			}
		}
		return ret;
	}
	
	public static final class ICounter {
		
        private int value = 0;
        
        public int next() {
            return ++value;
        }
		
		public int value() {
			return value;
		}
    }
	
	public static final class IPatterns {
		
		private final List<Pattern> patterns = new ArrayList<>();
		
		public IPatterns add(IPatterns values) {
			patterns.addAll(values.patterns);
			return this;
		}
		
		public IPatterns add(String value) {
			return add(Pattern.compile(Pattern.quote(value)));
		}
		
		public IPatterns add(Pattern value) {
			patterns.add(value);
			return this;
		}
		
		public IPatterns clear() {
			patterns.clear();
			return this;
		}
		
		public boolean matches(String value) {
			return patterns.stream().anyMatch(p -> p.matcher(value).matches());
		}
	}
	
	public static final class IField {
		
        private final Object object;
        private final Field field;
        private final String name;
        private final Object value;

        public IField(Object object, Field field, String name) {
            try {
                this.object = object;
                this.field = field;
				this.name = name;
                this.value = field.get(object);
            } catch(ReflectiveOperationException cause) {
                throw rethrow(cause);
            }
        }
		
		public Class<?> type() {
			 return field.getType();
		}
		
		public String name() {
			return name;
		}

        public void set(Object value) {
            try {
                field.set(object, value);
            } catch(ReflectiveOperationException cause) {
                throw rethrow(cause);
            }
        }
        
        public Object get() {
            try {
                return field.get(object);
            } catch(ReflectiveOperationException cause) {
                throw rethrow(cause);
            }
        }
        
        public void reset() {
            try {
                field.set(object, value);
            } catch(ReflectiveOperationException cause) {
                throw rethrow(cause);
            }
        }
        
    }
	
	public static RuntimeException rethrow(Throwable cause) {
        new ExceptionErasure<RuntimeException>().rethrow(cause);
        
        // not reachable, wyjątek bez wrappowania leci linię wyżej
        throw new RuntimeException(cause); // NOPMD
    }
    
    private static class ExceptionErasure<T extends Throwable> { // NOPMD
        
        @SuppressWarnings("unchecked")
        private void rethrow(Throwable exception) throws T {
            throw (T) exception;
        }

    }
	
}
