/*******************************************************************************
 * Copyright (c) 2006, 2014 Wind River Systems, Inc. and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Markus Schorn - initial API and implementation
 *     Andrew Ferguson (Symbian)
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.core.testplugin.util;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestFailure;
import junit.framework.TestResult;
import junit.framework.TestSuite;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ElementChangedEvent;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.IElementChangedListener;
import org.eclipse.cdt.core.testplugin.ResourceHelper;
import org.eclipse.cdt.core.testplugin.TestScannerProvider;
import org.eclipse.cdt.internal.core.CCoreInternals;
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPASTNameBase;
import org.eclipse.cdt.internal.core.pdom.CModelListener;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ILogListener;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;

public class BaseTestCase extends TestCase {
	private static final String DEFAULT_INDEXER_TIMEOUT_SEC = "10";
	private static final String INDEXER_TIMEOUT_PROPERTY = "indexer.timeout";
	/**
	 * Indexer timeout used by tests. To avoid this timeout expiring during debugging add
	 * -Dindexer.timeout=some_large_number to VM arguments of the test launch configuration. 
	 */
	protected static final int INDEXER_TIMEOUT_SEC =
			Integer.parseInt(System.getProperty(INDEXER_TIMEOUT_PROPERTY, DEFAULT_INDEXER_TIMEOUT_SEC));
	protected static final int INDEXER_TIMEOUT_MILLISEC= INDEXER_TIMEOUT_SEC * 1000;
	
	private boolean fExpectFailure;
	private int fBugNumber;
	private int fExpectedLoggedNonOK;
	private Deque<File> filesToDeleteOnTearDown= new ArrayDeque<>();

	public BaseTestCase() {
		super();
	}

	public BaseTestCase(String name) {
		super(name);
	}

	public static NullProgressMonitor npm() {
		return new NullProgressMonitor();
	}

	@Override
	protected void setUp() throws Exception {
		super.setUp();
		CPPASTNameBase.sAllowRecursionBindings= false;
		CPPASTNameBase.sAllowNameComputation= false;
		CModelListener.sSuppressUpdateOfLastRecentlyUsed= true;
	}

	@Override
	protected void tearDown() throws Exception {
		for (File file; (file = filesToDeleteOnTearDown.pollLast()) != null;) {
			file.delete();
		}
		ResourceHelper.cleanUp();
		TestScannerProvider.clear();
		super.tearDown();
	}

	protected void deleteOnTearDown(File file) {
		filesToDeleteOnTearDown.add(file);
	}

	protected File createTempFile(String prefix, String suffix) throws IOException {
		File file = File.createTempFile(prefix, suffix);
		filesToDeleteOnTearDown.add(file);
		return file;
	}

	protected File nonExistentTempFile(String prefix, String suffix) {
		File file= new File(System.getProperty("java.io.tmpdir"),
				prefix + System.currentTimeMillis() + suffix);
		filesToDeleteOnTearDown.add(file);
		return file;
	}

	protected static TestSuite suite(Class clazz) {
		return suite(clazz, null);
	}

	protected static TestSuite suite(Class clazz, String failingTestPrefix) {
		TestSuite suite= new TestSuite(clazz);
		Test failing= getFailingTests(clazz, failingTestPrefix);
		if (failing != null) {
			suite.addTest(failing);
		}
		return suite;
	}

	private static Test getFailingTests(Class clazz, String prefix) {
		TestSuite suite= new TestSuite("Failing Tests");
		HashSet names= new HashSet();
		Class superClass= clazz;
		while (Test.class.isAssignableFrom(superClass) && !TestCase.class.equals(superClass)) {
			Method[] methods= superClass.getDeclaredMethods();
			for (Method method : methods) {
				addFailingMethod(suite, method, names, clazz, prefix);
			}
			superClass= superClass.getSuperclass();
		}
		if (suite.countTestCases() == 0) {
			return null;
		}
		return suite;
	}

	private static void addFailingMethod(TestSuite suite, Method m, Set names, Class clazz, String prefix) {
		String name= m.getName();
		if (!names.add(name)) {
			return;
		}
		if (name.startsWith("test") || (prefix != null && !name.startsWith(prefix))) {
			return;
		}
		if (name.equals("tearDown") || name.equals("setUp") || name.equals("runBare")) {
			return;
		}
		if (Modifier.isPublic(m.getModifiers())) {
			Class[] parameters= m.getParameterTypes();
			Class returnType= m.getReturnType();
			if (parameters.length == 0 && returnType.equals(Void.TYPE)) {
				Test test= TestSuite.createTest(clazz, name);
				((BaseTestCase) test).setExpectFailure(0);
				suite.addTest(test);
			}
		}
	}

	@Override
	public void runBare() throws Throwable {
		final List<IStatus> statusLog= Collections.synchronizedList(new ArrayList());
		ILogListener logListener= new ILogListener() {
			@Override
			public void logging(IStatus status, String plugin) {
				if (!status.isOK() && status.getSeverity() != IStatus.INFO) {
					switch (status.getCode()) {
					case IResourceStatus.NOT_FOUND_LOCAL:
					case IResourceStatus.NO_LOCATION_LOCAL:
					case IResourceStatus.FAILED_READ_LOCAL:
					case IResourceStatus.RESOURCE_NOT_LOCAL:
						// Logged by the resources plugin.
						return;
					}
					statusLog.add(status);
				}
			}
		};
		final CCorePlugin corePlugin = CCorePlugin.getDefault();
		if (corePlugin != null) { // Iff we don't run as a JUnit Plugin Test.
			corePlugin.getLog().addLogListener(logListener);
		}

		Throwable testThrowable= null;
		try {
			try {
				super.runBare();
			} catch (Throwable e) {
				testThrowable= e;
			}

			if (statusLog.size() != fExpectedLoggedNonOK) {
				StringBuilder msg= new StringBuilder("Expected number (" + fExpectedLoggedNonOK + ") of ");
				msg.append("Non-OK status objects in log differs from actual (" + statusLog.size() + ").\n");
				Throwable cause= null;
				if (!statusLog.isEmpty()) {
					synchronized (statusLog) {
						for (IStatus status : statusLog) {
							IStatus[] ss= {status};
							ss= status instanceof MultiStatus ? ((MultiStatus) status).getChildren() : ss;
							for (IStatus s : ss) {
								msg.append("\t" + s.getMessage() + " ");

								Throwable t= s.getException();
								cause= cause != null ? cause : t;
								if (t != null) {
									msg.append(t.getMessage() != null ? t.getMessage() : t.getClass().getCanonicalName());
								}

								msg.append("\n");
							}
						}
					}
				}
				cause= cause != null ? cause : testThrowable;
				AssertionFailedError afe= new AssertionFailedError(msg.toString());
				afe.initCause(cause);
				throw afe;
			}
		} finally {
			if (corePlugin != null) {
				corePlugin.getLog().removeLogListener(logListener);
			}
		}

		if (testThrowable != null)
			throw testThrowable;
	}

    @Override
	public void run(TestResult result) {
    	if (!fExpectFailure || Boolean.parseBoolean(System.getProperty("SHOW_EXPECTED_FAILURES"))) {
    		super.run(result);
    		return;
    	}

        result.startTest(this);

        TestResult r = new TestResult();
        super.run(r);
        if (r.failureCount() == 1) {
        	TestFailure failure= r.failures().nextElement();
        	String msg= failure.exceptionMessage();
        	if (msg != null && msg.startsWith("Method \"" + getName() + "\"")) {
        		result.addFailure(this, new AssertionFailedError(msg));
        	}
        } else if (r.errorCount() == 0 && r.failureCount() == 0) {
            String err = "Unexpected success of " + getName();
            if (fBugNumber > 0) {
                err += ", bug #" + fBugNumber;
            }
            result.addFailure(this, new AssertionFailedError(err));
        }

        result.endTest(this);
    }

    public void setExpectFailure(int bugNumber) {
    	fExpectFailure= true;
    	fBugNumber= bugNumber;
    }

    /**
     * The last value passed to this method in the body of a testXXX method
     * will be used to determine whether or not the presence of non-OK status objects
     * in the log should fail the test. If the logged number of non-OK status objects
     * differs from the last value passed, the test is failed. If this method is not called
     * at all, the expected number defaults to zero.
     * @param count the expected number of logged error and warning messages
     */
    public void setExpectedNumberOfLoggedNonOKStatusObjects(int count) {
    	fExpectedLoggedNonOK= count;
    }

    /**
     * Some test steps need synchronizing against a CModel event. This class
     * is a very basic means of doing that.
     */
    static protected class ModelJoiner implements IElementChangedListener {
		private final boolean[] changed= new boolean[1];

		public ModelJoiner() {
			CoreModel.getDefault().addElementChangedListener(this);
		}

		public void clear() {
			synchronized (changed) {
				changed[0]= false;
				changed.notifyAll();
			}
		}

		public void join() throws CoreException {
			try {
				synchronized(changed) {
					while (!changed[0]) {
						changed.wait();
					}
				}
			} catch (InterruptedException e) {
				throw new CoreException(CCorePlugin.createStatus("Interrupted", e));
			}
		}

		public void dispose() {
			CoreModel.getDefault().removeElementChangedListener(this);
		}

		@Override
		public void elementChanged(ElementChangedEvent event) {
			// Only respond to post change events
			if (event.getType() != ElementChangedEvent.POST_CHANGE)
				return;

			synchronized (changed) {
				changed[0]= true;
				changed.notifyAll();
			}
		}
	}

    public static void waitForIndexer(ICProject project) throws InterruptedException {
		Job.getJobManager().join(ResourcesPlugin.FAMILY_AUTO_REFRESH, null);
		assertTrue(CCoreInternals.getPDOMManager().joinIndexer(INDEXER_TIMEOUT_SEC * 1000, npm()));
	}

	public static void waitUntilFileIsIndexed(IIndex index, IFile file) throws Exception {
		TestSourceReader.waitUntilFileIsIndexed(index, file, INDEXER_TIMEOUT_SEC * 1000);
	}
}
