/*
 * @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.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import net.ranides.assira.junit.TestUtils.ICounter;
import net.ranides.assira.junit.TestUtils.IField;
import net.ranides.assira.junit.TestUtils.IPatterns;
import org.junit.Assert;
import org.junit.Ignore;

/**
 * Inspects inheritance tree of objects passed to {@link #run} method and invokes
 * all compatible testers registered by {#append} method. For example, if you
 * register testers annotated as compatible with {@code Map} & {@code SortedMap}, 
 * then method {@code run} will invoke both of them for {@code TreeMap}, but only
 * the first one for {@codee HashMap}.
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class TestContractRunner {
	
	// @todo (assira.junit #3) obsługa wielu funkcji i supplierów
	// możemy pobierać typy argumentów i returnów funkcji za pomocą TestUtils#typeof(Function)
	// jeśli tę funkcję rozszerzyć, żeby zwracała Tuple<Param,Result> to mamy wszystko
	// potem to można trzymać w jakiejś mapie
	// a potem można matchować przy uruchamianiu metod
	//
	// w sumie przydatne, jeśli nasza klasa chce ruszyć na kilku testerach, które
	// pobierają różne inputy:
	//		Function<int[], MyInterface>
	//		Function<char[], MyInterface>
	//		Function<byte[], MyInterface>
	//		Function<String, MyInterface>
	//		Supplier<MyInterface>
	
	private static final boolean DEBUG = "true".equals(System.getProperty("assira.junit.debug"));
	
	private final SortedSet<MethodRunner> methods = new TreeSet<>();
    
    private final List<TesterWrapper> testers = new ArrayList<>();
    
    private final IPatterns ignored = new IPatterns();
        
    private final PrintStream output;
	
	private final Generator generator = new Generator();
    
    private boolean debug = DEBUG;
	
	private Function<Object, Object> function;
	
	private Supplier<Object> supplier;
	

    public TestContractRunner(PrintStream output) {
        this.output = output;
    }
    
    public TestContractRunner(TestContractRunner runner) {
        this(runner.output);
		for(TesterWrapper w : runner.testers) {
			appendWrapper(TestUtils.copy(w.object)).setParams(w);
		}
		this.ignored.add(runner.ignored);
		this.debug = runner.debug;
    }
    
	public TestContractRunner debug(boolean value) {
        this.debug = value;
		return this;
	}
    
    public boolean debug() {
        return this.debug;
	}
	
    public TestContractRunner ignore(String method) {
        this.ignored.add(method);
        return this;
    }
	
	public TestContractRunner ignore(Pattern method) {
        this.ignored.add(method);
        return this;
    }
    
    public TestContractRunner append(Object tester) {
        appendWrapper(tester);
        return this;
    }
	
	TesterWrapper appendWrapper(Object tester) {
        TesterWrapper wrapper = new TesterWrapper(tester);
        testers.add(wrapper);
        for(Method m : tester.getClass().getMethods()) {
            if(Modifier.isStatic(m.getModifiers()) ) {
                continue;
            }
            if(1 != m.getParameterCount()) {
                continue;
            }
            if(null != m.getAnnotation(Ignore.class)) {
                continue;
            }
            if(null == m.getAnnotation(TestContract.class)) {
                continue;
            }
            if(!m.getReturnType().equals(void.class)) {
                continue;
            }
			methods.add(runner(tester, m));
        }
		return wrapper;
	}
    
    public TestContractRunner param(String name, Object value) {
        testers.stream().forEach(t -> t.param(name, value));
        return this;
    }
    
    public TestContractRunner apply(Consumer<TestContractRunner> configurator) {
        configurator.accept(this);
        return this;
    }
    
	@SuppressWarnings("unchecked")
	public TestContractRunner supplier(Supplier<?> supplier) {
		this.supplier = (Supplier)supplier;
		return this;
	}
	
    @SuppressWarnings("unchecked")
	public <T,R> TestContractRunner function(Function<T, R> function) {
		this.function = (Function)function;
		return this;
	}
	
	public <T,R> TestContractRunner function(T ident, Function<T, R> function) {
		function(function);
		this.supplier = () -> this.function.apply(ident);
		return this;
	}

    public boolean run() {
        for(TesterWrapper tester : testers) {
            tester.bind(this);
        }
		if(null == supplier && null == function) {
			throw new AssertionError("You have to set Supplier or Function.");
		}
		assert null != supplier: "Unknown supplier/function type.";
        Class<?> type = supplier.get().getClass();

        ICounter counter = new ICounter();
        List<String> failed = new ArrayList<>();
        methods.stream().filter(m -> m.accept(type)).forEach(m -> {
			m.run(counter, cause -> {
				failed.add(m.label);
				cause.printStackTrace(output);
			});
        });
        ireset();
        if(!failed.isEmpty()) {
            output.println();
            output.println("Failed interface tests: ");
            for(String f : failed) {
                output.print(f);
            }
            output.println();
            Assert.fail("Failed interface tests: " + failed.size() + "/"+counter.value());
        }
		return true;
	}
    
    private void ireset() {
		debug = DEBUG;
        ignored.clear();
		function = null;
		supplier = null;
        for(TesterWrapper tester : testers) {
            tester.resetParams();
        }
    }
    
    private MethodRunner runner(Object that, Method method) {
        if( TestUtils.acceptsSupplier(method) ) {
            return new SMethodRunner(that, method);
        }
        if( TestUtils.acceptsFunction(method) ) {
            return new SMethodRunner(that, method);
        }
        return new CMethodRunner(that, method);
    }
    
    private class CMethodRunner extends MethodRunner {

        public CMethodRunner(Object that, Method method) {
            super(that, method);
        }

        @Override
        protected void invoke() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
			if(null == supplier) {
				throw new AssertionError("Supplier is required by method: " + nid, TestUtils.cause4method(method, "Supplier required."));
			}
            method.invoke(that, supplier.get());
        }
        
    }
    
    private class SMethodRunner extends MethodRunner {

        public SMethodRunner(Object that, Method method) {
            super(that, method);
        }
        
        @Override
        protected void invoke() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            method.invoke(that, generator);
        }
        
    }
    
    private abstract class MethodRunner implements Comparable<MethodRunner> {
        
        protected final String uid;
        
        protected final String nid;
        
        protected final Class<?> type;
        
        protected final Object that;
        
        protected final Method method;
        
        protected String label;

        public MethodRunner(Object that, Method method) {
            this.type = TestUtils.typeof(method);
            this.that = that;
            this.method = method;
            this.uid = TestUtils.uid(method);
            this.nid = TestUtils.nid(method);
        }
        
        @Override
        public int compareTo(MethodRunner object) {
            return uid.compareTo(object.uid);
        }
        
        public boolean accept(Class<?> param) {
            return type.isAssignableFrom(param);
        }
        
        protected abstract void invoke() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
        
        public void run(ICounter counter, Consumer<Throwable> handler) {
			if(ignored.matches(nid)) {
				if(debug) {
					printf(type, counter.next(), method, "IGNORED");
				}
				return;
			}
            int index = counter.next();
            try {
                invoke();
				if(debug) {
					printf(type, index, method, "");
				}
            } catch (IllegalAccessException | IllegalArgumentException ex) {
                handler.accept(ex);
            } catch (InvocationTargetException ex) {
				printf(type, index, method, "FAILED");
				handler.accept(ex.getTargetException());
            }
        }
        
        private void printf(Class<?> type, int index, Method method, String comment) {
            if(null == comment || comment.isEmpty()) {
                comment = "";
            } else {
                comment = " - " + comment;
            }
            label = String.format(" - SUITE: %s [%d] %s.%s%s%n",
                type.getName(),
                index,
                method.getDeclaringClass().getSimpleName(),
                method.getName(),
                comment
            );
            output.print(label);
        }
        
    }
    
    private static final class TesterWrapper {
        
        private final Object object;
        
        private final Map<String, IField> resources = new HashMap<>();
        
        private final List<IField> bindings = new ArrayList<>();
        
        public TesterWrapper(Object object) {
            this.object = object;
			for(IField f : TestUtils.getFields(object)) {
				if( TestContractRunner.class.equals(f.type()) ) {
					bindings.add(f);
				} else {
					resources.put(f.name(), f);
				}
			}
        }
                
        public void bind(TestContractRunner runner) {
            for(IField p : bindings) {
                p.set(runner);
            }
        }
        
        public void param(String name, Object value) {
            if( resources.containsKey(name) ) {
                resources.get(name).set(value);
            }
        }
		
		public void setParams(TesterWrapper source) {
			for(Entry<String, IField> entry : source.resources.entrySet()) {
				String key = entry.getKey();
				Object val = entry.getValue().get();
				resources.get(key).set(val);
			}
		}
        
        public void resetParams() {
            for(IField param : resources.values()) {
                param.reset();
            }
        }
        
    }
	
	private final class Generator implements Function<Object, Object>, Supplier<Object> {

		@Override
		public Object apply(Object params) {
			if( null == function) {
				throw new AssertionError("Function is not set.");
			}
			return function.apply(params);
		}

		@Override
		public Object get() {
			if(null == supplier) {
				throw new AssertionError("Supplier is not set.");
			}
			return supplier.get();
		}
		
	}
	
}
