package com.xtremelabs.robolectric;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import javassist.Loader;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import android.app.Application;
import android.net.Uri__FromAndroid;

import com.xtremelabs.robolectric.bytecode.ClassHandler;
import com.xtremelabs.robolectric.bytecode.RobolectricClassLoader;
import com.xtremelabs.robolectric.bytecode.ShadowWrangler;
import com.xtremelabs.robolectric.internal.RealObject;
import com.xtremelabs.robolectric.internal.RobolectricTestRunnerInterface;
import com.xtremelabs.robolectric.res.ResourceLoader;
import com.xtremelabs.robolectric.shadows.ShadowApplication;
import com.xtremelabs.robolectric.shadows.ShadowLog;
import com.xtremelabs.robolectric.util.DatabaseConfig;
import com.xtremelabs.robolectric.util.DatabaseConfig.DatabaseMap;
import com.xtremelabs.robolectric.util.DatabaseConfig.UsingDatabaseMap;
import com.xtremelabs.robolectric.util.SQLiteMap;

/**
 * Installs a {@link RobolectricClassLoader} and {@link com.xtremelabs.robolectric.res.ResourceLoader} in order to
 * provide a simulation of the Android runtime environment.
 */
public class RobolectricTestRunner extends BlockJUnit4ClassRunner implements RobolectricTestRunnerInterface {

    private static final String MANIFEST_PATH_PROPERTY = "robolectric.path.manifest";
    private static final String RES_PATH_PROPERTY = "robolectric.path.res";
    private static final String ASSETS_PATH_PROPERTY = "robolectric.path.assets";
    private static final String DEFAULT_MANIFEST_PATH = "./AndroidManifest.xml";
    private static final String DEFAULT_RES_PATH = "./res";
    private static final String DEFAULT_ASSETS_PATH = "./assets";

    private static final Logger logger =
            Logger.getLogger(RobolectricTestRunner.class.getSimpleName());

    /** Instrument detector. We use it to check whether the current instance is instrumented. */
  	private static InstrumentDetector instrumentDetector = InstrumentDetector.DEFAULT;

    private static RobolectricClassLoader defaultLoader;
    private static Map<RobolectricConfig, ResourceLoader> resourceLoaderForRootAndDirectory = new HashMap<RobolectricConfig, ResourceLoader>();

    // fields in the RobolectricTestRunner in the original ClassLoader
    private RobolectricClassLoader classLoader;
    private ClassHandler classHandler;
    private RobolectricTestRunnerInterface delegate;
    private DatabaseMap databaseMap;

	// fields in the RobolectricTestRunner in the instrumented ClassLoader
    protected RobolectricConfig robolectricConfig;

    private static RobolectricClassLoader getDefaultLoader() {
        if (defaultLoader == null) {
            defaultLoader = new RobolectricClassLoader(ShadowWrangler.getInstance());
        }
        return defaultLoader;
    }

    public static void setInstrumentDetector(final InstrumentDetector detector) {
      instrumentDetector = detector;
    }

    public static void setDefaultLoader(Loader robolectricClassLoader) {
    	//used by the RoboSpecs project to allow for mixed scala\java tests to be run with Maven Surefire (see the RoboSpecs project on github)
        if (defaultLoader == null) {
            defaultLoader = (RobolectricClassLoader)robolectricClassLoader;
        } else throw new RuntimeException("You may not set the default robolectricClassLoader unless it is null!");
    }

    /**
     * Call this if you would like Robolectric to rewrite additional classes and turn them
     * into "do nothing" classes which proxy all method calls to shadow classes, just like it does
     * with the android classes by default.
     *
     * @param classOrPackageToBeInstrumented fully-qualified class or package name
     */
    protected static void addClassOrPackageToInstrument(String classOrPackageToBeInstrumented) {
        if (!isInstrumented()) {
            defaultLoader.addCustomShadowClass(classOrPackageToBeInstrumented);
        }
    }

    /**
     * Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file
     * and res directory.
     *
     * @param testClass the test class to be run
     * @throws InitializationError if junit says so
     */
    public RobolectricTestRunner(final Class<?> testClass) throws InitializationError {
        this(testClass, new RobolectricConfig(
                new File(getSystemProperty(MANIFEST_PATH_PROPERTY, DEFAULT_MANIFEST_PATH)),
                new File(getSystemProperty(RES_PATH_PROPERTY, DEFAULT_RES_PATH)),
                new File(getSystemProperty(ASSETS_PATH_PROPERTY, DEFAULT_ASSETS_PATH))));
    }

    /**
     * Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file
     * and res directory.
     *
     * @param testClass the test class to be run
     * @param classLoader a custom RobolectricClassLoader to be used.
     * @throws InitializationError if junit says so
     */
    public RobolectricTestRunner(final Class<?> testClass, RobolectricClassLoader classLoader)
            throws InitializationError {
        this(testClass,
            isInstrumented() ? null : ShadowWrangler.getInstance(),
            isInstrumented() ? null : classLoader,
            new RobolectricConfig(
                new File(getSystemProperty(MANIFEST_PATH_PROPERTY, DEFAULT_MANIFEST_PATH)),
                new File(getSystemProperty(RES_PATH_PROPERTY, DEFAULT_RES_PATH)),
                new File(getSystemProperty(ASSETS_PATH_PROPERTY, DEFAULT_ASSETS_PATH))));
    }

    /**
     * Call this constructor in subclasses in order to specify non-default configuration (e.g. location of the
     * AndroidManifest.xml file and resource directory).
     *
     * @param testClass         the test class to be run
     * @param robolectricConfig the configuration data
     * @throws InitializationError if junit says so
     */
    protected RobolectricTestRunner(final Class<?> testClass, final RobolectricConfig robolectricConfig)
            throws InitializationError {
        this(testClass,
                isInstrumented() ? null : ShadowWrangler.getInstance(),
                isInstrumented() ? null : getDefaultLoader(),
                robolectricConfig, new SQLiteMap());
    }

    /**
     * Call this constructor in subclasses in order to specify non-default configuration (e.g. location of the
     * AndroidManifest.xml file, resource directory, and DatabaseMap).
     *
     * @param testClass         the test class to be run
     * @param robolectricConfig the configuration data
     * @param databaseMap		the database mapping
     * @throws InitializationError if junit says so
     */
    protected RobolectricTestRunner(Class<?> testClass, RobolectricConfig robolectricConfig, DatabaseMap databaseMap)
            throws InitializationError {
        this(testClass,
                isInstrumented() ? null : ShadowWrangler.getInstance(),
                isInstrumented() ? null : getDefaultLoader(),
                robolectricConfig, databaseMap);
    }

    /**
     * Call this constructor in subclasses in order to specify the project root directory.
     *
     * @param testClass          the test class to be run
     * @param androidProjectRoot the directory containing your AndroidManifest.xml file and res dir
     * @throws InitializationError if the test class is malformed
     */
    public RobolectricTestRunner(final Class<?> testClass, final File androidProjectRoot) throws InitializationError {
        this(testClass, new RobolectricConfig(androidProjectRoot));
    }

    /**
     * Call this constructor in subclasses in order to specify the project root directory.
     *
     * @param testClass          the test class to be run
     * @param androidProjectRoot the directory containing your AndroidManifest.xml file and res dir
     * @throws InitializationError if junit says so
     * @deprecated Use {@link #RobolectricTestRunner(Class, File)} instead.
     */
    @Deprecated
    public RobolectricTestRunner(final Class<?> testClass, final String androidProjectRoot) throws InitializationError {
        this(testClass, new RobolectricConfig(new File(androidProjectRoot)));
    }

    /**
     * Call this constructor in subclasses in order to specify the location of the AndroidManifest.xml file and the
     * resource directory. The #androidManifestPath is used to locate the AndroidManifest.xml file which, in turn,
     * contains package name for the {@code R} class which contains the identifiers for all of the resources. The
     * resource directory is where the resource loader will look for resources to load.
     *
     * @param testClass           the test class to be run
     * @param androidManifestPath the AndroidManifest.xml file
     * @param resourceDirectory   the directory containing the project's resources
     * @throws InitializationError if junit says so
     */
    protected RobolectricTestRunner(final Class<?> testClass, final File androidManifestPath, final File resourceDirectory)
            throws InitializationError {
        this(testClass, new RobolectricConfig(androidManifestPath, resourceDirectory));
    }

    /**
     * Call this constructor in subclasses in order to specify the location of the AndroidManifest.xml file and the
     * resource directory. The #androidManifestPath is used to locate the AndroidManifest.xml file which, in turn,
     * contains package name for the {@code R} class which contains the identifiers for all of the resources. The
     * resource directory is where the resource loader will look for resources to load.
     *
     * @param testClass           the test class to be run
     * @param androidManifestPath the relative path to the AndroidManifest.xml file
     * @param resourceDirectory   the relative path to the directory containing the project's resources
     * @throws InitializationError if junit says so
     * @deprecated Use {@link #RobolectricTestRunner(Class, File, File)} instead.
     */
    @Deprecated
    protected RobolectricTestRunner(final Class<?> testClass, final String androidManifestPath, final String resourceDirectory)
            throws InitializationError {
        this(testClass, new RobolectricConfig(new File(androidManifestPath), new File(resourceDirectory)));
    }

    protected RobolectricTestRunner(Class<?> testClass, ClassHandler classHandler, RobolectricClassLoader classLoader, RobolectricConfig robolectricConfig) throws InitializationError {
        this(testClass, classHandler, classLoader, robolectricConfig, new SQLiteMap());
    }


    /**
     * This is not the constructor you are looking for... probably. This constructor creates a bridge between the test
     * runner called by JUnit and a second instance of the test runner that is loaded via the instrumenting class
     * loader. This instrumented instance of the test runner, along with the instrumented instance of the actual test,
     * provides access to Robolectric's features and the un-instrumented instance of the test runner delegates most of
     * the interesting test runner behavior to it. Providing your own class handler and class loader here in order to
     * get different functionality is a difficult and dangerous project. If you need to customize the project root and
     * resource directory, use {@link #RobolectricTestRunner(Class, String, String)}. For other extensions, consider
     * creating a subclass and overriding the documented methods of this class.
     *
     * @param testClass         the test class to be run
     * @param classHandler      the {@link ClassHandler} to use to in shadow delegation
     * @param classLoader       the {@link RobolectricClassLoader}
     * @param robolectricConfig the configuration
     * @throws InitializationError if junit says so
     */
    protected RobolectricTestRunner(final Class<?> testClass, final ClassHandler classHandler, final RobolectricClassLoader classLoader, final RobolectricConfig robolectricConfig, final DatabaseMap map) throws InitializationError {
        super(isInstrumented() ? testClass
            : ensureClassLoaderNotNull(classLoader).bootstrap(testClass));

        if (!isInstrumented()) {
            this.classHandler = classHandler;
            this.classLoader = ensureClassLoaderNotNull(classLoader);
            this.robolectricConfig = robolectricConfig;
            this.databaseMap = setupDatabaseMap(testClass, map);

            Thread.currentThread().setContextClassLoader(classLoader);

            delegateLoadingOf(Uri__FromAndroid.class.getName());
            delegateLoadingOf(RobolectricTestRunnerInterface.class.getName());
            delegateLoadingOf(RealObject.class.getName());
            delegateLoadingOf(ShadowWrangler.class.getName());
            delegateLoadingOf(RobolectricConfig.class.getName());
            delegateLoadingOf(DatabaseMap.class.getName());
            delegateLoadingOf(android.R.class.getName());

            Class<?> delegateClass = classLoader.bootstrap(this.getClass());
            try {
                Constructor<?> constructorForDelegate = delegateClass.getConstructor(Class.class);
                this.delegate = (RobolectricTestRunnerInterface) constructorForDelegate.newInstance(classLoader.bootstrap(testClass));
                this.delegate.setRobolectricConfig(robolectricConfig);
                this.delegate.setDatabaseMap(databaseMap);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    private static RobolectricClassLoader ensureClassLoaderNotNull(
        RobolectricClassLoader classLoader) {
        return classLoader == null ? getDefaultLoader() : classLoader;
    }

    protected static boolean isInstrumented() {
        return instrumentDetector.isInstrumented();
    }

    /**
     * Only used when creating the delegate instance within the instrumented ClassLoader.
     * <p/>
     * This is not the constructor you are looking for.
     */
    @SuppressWarnings({"UnusedDeclaration", "JavaDoc"})
    protected RobolectricTestRunner(final Class<?> testClass, final ClassHandler classHandler, final RobolectricConfig robolectricConfig) throws InitializationError {
        super(testClass);
        this.classHandler = classHandler;
        this.robolectricConfig = robolectricConfig;
    }

    /** @deprecated use {@link Robolectric.Reflection#setFinalStaticField(Class, String, Object)} */
    @Deprecated
    public static void setStaticValue(Class<?> clazz, String fieldName, Object value) {
        Robolectric.Reflection.setFinalStaticField(clazz, fieldName, value);
    }

    protected void delegateLoadingOf(final String className) {
        classLoader.delegateLoadingOf(className);
    }

    @Override protected Statement methodBlock(final FrameworkMethod method) {
        setupI18nStrictState(method.getMethod(), robolectricConfig);
        lookForLocaleAnnotation( method.getMethod(), robolectricConfig );

    	if (classHandler != null) {
            classHandler.configure(robolectricConfig);
            classHandler.beforeTest();
        }
        delegate.internalBeforeTest(method.getMethod());

        final Statement statement = super.methodBlock(method);
        return new Statement() {
            @Override public void evaluate() throws Throwable {
                // todo: this try/finally probably isn't right -- should mimic RunAfters? [xw]
                try {
                    statement.evaluate();
                } finally {
                    delegate.internalAfterTest(method.getMethod());
                    if (classHandler != null) {
                        classHandler.afterTest();
                    }
                }
            }
        };
    }

    /*
     * Called before each test method is run. Sets up the simulation of the Android runtime environment.
     */
    @Override public void internalBeforeTest(final Method method) {
        setupApplicationState(robolectricConfig);

        beforeTest(method);
    }

    @Override public void internalAfterTest(final Method method) {
        afterTest(method);
    }

    @Override public void setRobolectricConfig(final RobolectricConfig robolectricConfig) {
        this.robolectricConfig = robolectricConfig;
    }

    /**
     * Called before each test method is run.
     *
     * @param method the test method about to be run
     */
    public void beforeTest(final Method method) {
    }

    /**
     * Called after each test method is run.
     *
     * @param method the test method that just ran.
     */
    public void afterTest(final Method method) {
    }

    /**
     * You probably don't want to override this method. Override #prepareTest(Object) instead.
     *
     * @see BlockJUnit4ClassRunner#createTest()
     */
    @Override
    public Object createTest() throws Exception {
        if (delegate != null) {
            return delegate.createTest();
        } else {
            Object test = super.createTest();
            prepareTest(test);
            return test;
        }
    }

    public void prepareTest(final Object test) {
    }

    public void setupApplicationState(final RobolectricConfig robolectricConfig) {
        setupLogging();

        ResourceLoader resourceLoader = createResourceLoader(robolectricConfig );

        Robolectric.bindDefaultShadowClasses();
        bindShadowClasses();

        resourceLoader.setLayoutQualifierSearchPath();
        Robolectric.resetStaticState();
        resetStaticState();

        DatabaseConfig.setDatabaseMap(this.databaseMap);//Set static DatabaseMap in DBConfig

        Robolectric.application = ShadowApplication.bind(createApplication(), resourceLoader);
    }

    /**
     * Override this method to bind your own shadow classes
     */
    protected void bindShadowClasses() {
    }

    /**
     * Override this method to reset the state of static members before each test.
     */
    protected void resetStaticState() {
    }

    private static String getSystemProperty(String propertyName, String defaultValue) {
        String property = System.getProperty(propertyName);
        if (property == null) {
            property = defaultValue;
            logger.info("No system property " + propertyName + " found, default to "
                    + defaultValue);
        }
        return property;
    }

    /**
     * Sets Robolectric config to determine if Robolectric should blacklist API calls that are not
     * I18N/L10N-safe.
     * <p/>
     * I18n-strict mode affects suitably annotated shadow methods. Robolectric will throw exceptions
     * if these methods are invoked by application code. Additionally, Robolectric's ResourceLoader
     * will throw exceptions if layout resources use bare string literals instead of string resource IDs.
     * <p/>
     * To enable or disable i18n-strict mode for specific test cases, annotate them with
     * {@link com.xtremelabs.robolectric.annotation.EnableStrictI18n} or
     * {@link com.xtremelabs.robolectric.annotation.DisableStrictI18n}.
     * <p/>
     *
     * By default, I18n-strict mode is disabled.
     *
     * @param method
     * @param robolectricConfig
     */
    private void setupI18nStrictState(Method method, RobolectricConfig robolectricConfig) {
    	// Global
    	boolean strictI18n = globalI18nStrictEnabled();

    	// Test case class
    	Annotation[] annos = method.getDeclaringClass().getAnnotations();
    	strictI18n = lookForI18nAnnotations(strictI18n, annos);

    	// Test case methods
    	annos = method.getAnnotations();
    	strictI18n = lookForI18nAnnotations(strictI18n, annos);

		robolectricConfig.setStrictI18n(strictI18n);
    }

    /**
     * Default implementation of global switch for i18n-strict mode.
     * To enable i18n-strict mode globally, set the system property
     * "robolectric.strictI18n" to true. This can be done via java
     * system properties in either Ant or Maven.
     * <p/>
     * Subclasses can override this method and establish their own policy
     * for enabling i18n-strict mode.
     *
     * @return
     */
    protected boolean globalI18nStrictEnabled() {
    	return Boolean.valueOf(System.getProperty("robolectric.strictI18n"));
    }

    /**
     * As test methods are loaded by the delegate's class loader, the normal
 	 * method#isAnnotationPresent test fails. Look at string versions of the
     * annotation names to test for their presence.
     *
     * @param strictI18n
     * @param annos
     * @return
     */
	private boolean lookForI18nAnnotations(boolean strictI18n, Annotation[] annos) {
		for ( int i = 0; i < annos.length; i++ ) {
    		String name = annos[i].annotationType().getName();
    		if (name.equals("com.xtremelabs.robolectric.annotation.EnableStrictI18n")) {
    			strictI18n = true;
    			break;
    		}
    		if (name.equals("com.xtremelabs.robolectric.annotation.DisableStrictI18n")) {
    			strictI18n = false;
    			break;
    		}
    	}
		return strictI18n;
	}

	private void lookForLocaleAnnotation( Method method, RobolectricConfig robolectricConfig ){
		String locale = "";
		// TODO: there are maybe better implementation for getAnnotation
		// Have tried to use several other simple ways, but failed.
		Annotation[] annos = method.getDeclaredAnnotations();
		for( Annotation anno: annos ){

			if( anno.annotationType().getName().equals( "com.xtremelabs.robolectric.annotation.Values" )){
				String annotationString = anno.toString();
				int startIndex = annotationString.indexOf( '=' );
				int endIndex = annotationString.indexOf( ')' );

				if( startIndex < 0 || endIndex < 0 ){ return; }

				locale = annotationString.substring( startIndex + 1, endIndex );
			}
		}

		robolectricConfig.setLocale( locale );
	}

    private void setupLogging() {
        String logging = System.getProperty("robolectric.logging");
        if (logging != null && ShadowLog.stream == null) {
            PrintStream stream = null;
            if ("stdout".equalsIgnoreCase(logging)) {
                stream = System.out;
            } else if ("stderr".equalsIgnoreCase(logging)) {
                stream = System.err;
            } else {
                try {
                    final PrintStream file = new PrintStream(new FileOutputStream(logging));
                    stream = file;
                    Runtime.getRuntime().addShutdownHook(new Thread() {
                        @Override public void run() {
                            try { file.close(); } catch (Exception ignored) { }
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            ShadowLog.stream = stream;
        }
    }

    /**
     * Override this method if you want to provide your own implementation of Application.
     * <p/>
     * This method attempts to instantiate an application instance as specified by the AndroidManifest.xml.
     *
     * @return An instance of the Application class specified by the ApplicationManifest.xml or an instance of
     *         Application if not specified.
     */
    protected Application createApplication() {
        return new ApplicationResolver(robolectricConfig).resolveApplication();
    }

    private ResourceLoader createResourceLoader(final RobolectricConfig robolectricConfig) {
        ResourceLoader resourceLoader = resourceLoaderForRootAndDirectory.get(robolectricConfig);
        // When locale has changed, reload the resource files.
        if (resourceLoader == null || robolectricConfig.isLocaleChanged() ) {
            try {
                robolectricConfig.validate();

                String rClassName = robolectricConfig.getRClassName();
                Class rClass;
                try {
                    rClass = Class.forName(rClassName);
                } catch (ClassNotFoundException e) {
                    rClass = null;
                }
                resourceLoader = new ResourceLoader(robolectricConfig.getRealSdkVersion(), rClass, robolectricConfig.getResourceDirectory(), robolectricConfig.getAssetsDirectory(), robolectricConfig.getLocale() );
                resourceLoaderForRootAndDirectory.put(robolectricConfig, resourceLoader);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        resourceLoader.setStrictI18n(robolectricConfig.getStrictI18n());
        return resourceLoader;
    }

    private String findResourcePackageName(final File projectManifestFile) throws ParserConfigurationException, IOException, SAXException {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document doc = db.parse(projectManifestFile);

        String projectPackage = doc.getElementsByTagName("manifest").item(0).getAttributes().getNamedItem("package").getTextContent();

        return projectPackage + ".R";
    }

    /*
     * Specifies what database to use for testing (ex: H2 or Sqlite),
     * this will load H2 by default, the SQLite TestRunner version will override this.
     */
    protected DatabaseMap setupDatabaseMap(Class<?> testClass, DatabaseMap map) {
    	DatabaseMap dbMap = map;

    	if (testClass.isAnnotationPresent(UsingDatabaseMap.class)) {
	    	UsingDatabaseMap usingMap = testClass.getAnnotation(UsingDatabaseMap.class);
	    	if(usingMap.value()!=null){
	    		dbMap = Robolectric.newInstanceOf(usingMap.value());
	    	} else {
	    		if (dbMap==null)
		    		throw new RuntimeException("UsingDatabaseMap annotation value must provide a class implementing DatabaseMap");
	    	}
    	}
    	return dbMap;
    }

    public DatabaseMap getDatabaseMap() {
		return databaseMap;
	}

	@Override
  public void setDatabaseMap(DatabaseMap databaseMap) {
		this.databaseMap = databaseMap;
	}

	/**
	 * Detects whether current instance is already instrumented.
	 */
	public interface InstrumentDetector {

	    /** Default detector. */
	    InstrumentDetector DEFAULT = new InstrumentDetector() {
	        @Override
	        public boolean isInstrumented() {
	            return RobolectricTestRunner.class.getClassLoader().getClass().getName().contains(RobolectricClassLoader.class.getName());
	        }
	    };

	    /**
	     * @return true if current instance is already instrumented
	     */
	    boolean isInstrumented();

	}

}
