/*******************************************************************************
 * Copyright (c) 2007, 2011 BEA Systems, Inc.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    wharley@bea.com - initial API and implementation
 *    IBM Corporation - Fix for bug 341494
 *******************************************************************************/

package org.eclipse.jdt.compiler.apt.tests.processors.elementutils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;

import org.eclipse.jdt.compiler.apt.tests.processors.base.BaseProcessor;

/**
 * A processor that exercises the methods on the Elements utility.  To enable this processor, add 
 * -Aorg.eclipse.jdt.compiler.apt.tests.processors.elementutils.ElementUtilsProc to the command line.
 * @since 3.3
 */
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class ElementUtilsProc extends BaseProcessor
{
	// Initialized in collectElements()
	private TypeElement _elementF;
	private TypeElement _elementConstants;
	private TypeElement _elementFChild;
	private TypeElement _elementFEnum;
	private TypeElement _elementG;
	private TypeElement _elementH;
	private TypeElement _elementJ;
	private TypeElement _elementAnnoX;
	private ExecutableElement _annoXValue;
	private TypeElement _elementAnnoY;
	private ExecutableElement _annoYValue;

	// Always return false from this processor, because it supports "*".
	// The return value does not signify success or failure!
	@Override
	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
	{
		if (roundEnv.processingOver()) {
			// We're not interested in the postprocessing round.
			return false;
		}
		Map<String, String> options = processingEnv.getOptions();
		if (!options.containsKey(this.getClass().getName())) {
			// Disable this processor unless we are intentionally performing the test.
			return false;
		}
		
		if (!collectElements()) {
			return false;
		}
		
		if (!examineGetAllAnnotations()) {
			return false;
		}
		
		if (!examineGetAllMembers()) {
			return false;
		}
		
		if (!examineIsDeprecated()) {
			return false;
		}
		
		if (!examineBinaryName()) {
			return false;
		}
		
		if (!examineGetDocComment()) {
			return false;
		}
		
		if (!examineHidesField()) {
			return false;
		}
		
		if (!examineHidesClass()) {
			return false;
		}
		
		if (!examineHidesMethod()) {
			return false;
		}
		
		if (!examineOverrides()) {
			return false;
		}
		if (!examineGetConstantExpression()) {
			return false;
		}
		reportSuccess();
		return false;
	}

	/**
	 * Collect some elements that will be reused in various tests
	 * @return true if successful
	 */
	private boolean collectElements()
	{
		_elementF = _elementUtils.getTypeElement("targets.model.pc.F");
		if (_elementF == null || _elementF.getKind() != ElementKind.CLASS) {
			reportError("element F was not found or was not a class");
			return false;
		}
		_elementFChild = _elementUtils.getTypeElement("targets.model.pc.F.FChild");
		if (_elementFChild == null || _elementFChild.getKind() != ElementKind.CLASS) {
			reportError("element FChild was not found or was not a class");
			return false;
		}
		_elementFEnum = _elementUtils.getTypeElement("targets.model.pc.F.FEnum");
		if (_elementFEnum == null || _elementFEnum.getKind() != ElementKind.ENUM) {
			reportError("enum F.FEnum was not found or was not an enum");
			return false;
		}
		_elementG = _elementUtils.getTypeElement("targets.model.pc.G");
		if (_elementG == null || _elementG.getKind() != ElementKind.CLASS) {
			reportError("element G was not found or was not a class");
			return false;
		}
		_elementH = _elementUtils.getTypeElement("targets.model.pc.H");
		if (_elementH == null || _elementH.getKind() != ElementKind.CLASS) {
			reportError("element H was not found or was not a class");
			return false;
		}
		_elementJ = _elementUtils.getTypeElement("targets.model.pc.J");
		if (_elementJ == null || _elementJ.getKind() != ElementKind.CLASS) {
			reportError("element J was not found or was not a class");
			return false;
		}
		
		_elementAnnoX = _elementUtils.getTypeElement("targets.model.pc.AnnoX");
		if (null == _elementAnnoX || _elementAnnoX.getKind() != ElementKind.ANNOTATION_TYPE) {
			reportError("annotation type annoX was not found or was not an annotation");
			return false;
		}
		for (ExecutableElement method : ElementFilter.methodsIn(_elementAnnoX.getEnclosedElements())) {
			if ("value".equals(method.getSimpleName().toString())) {
				_annoXValue = method;
			}
		}
		if (null == _annoXValue) {
			reportError("Could not find value() method in annotation type AnnoX");
			return false;
		}
		
		_elementAnnoY = _elementUtils.getTypeElement("targets.model.pc.AnnoY");
		if (null == _elementAnnoY || _elementAnnoY.getKind() != ElementKind.ANNOTATION_TYPE) {
			reportError("annotation type annoY was not found or was not an annotation");
			return false;
		}
		for (ExecutableElement method : ElementFilter.methodsIn(_elementAnnoY.getEnclosedElements())) {
			if ("value".equals(method.getSimpleName().toString())) {
				_annoYValue = method;
			}
		}
		if (null == _annoYValue) {
			reportError("Could not find value() method in annotation type AnnoY");
			return false;
		}
		_elementConstants = _elementUtils.getTypeElement("targets.model.pc.Constants");
		if (_elementConstants == null || _elementConstants.getKind() != ElementKind.CLASS) {
			reportError("_elementConstants was not found or was not a class");
			return false;
		}
		return true;
	}
	
	/**
	 * Test the {@link Elements#getAllAnnotationMirrors()} method
	 * @return true if all tests passed
	 */
	private boolean examineGetAllAnnotations()
	{
		List<? extends AnnotationMirror> annotationsH = _elementUtils.getAllAnnotationMirrors(_elementH);
		if (null == annotationsH) {
			reportError("examineGetAllAnnotations: getAllAnnotationMirrors(_elementH) returned null");
			return false;
		}
		// H has AnnoY("on H"), G has AnnoX("on G"), and F has hidden AnnoY("on F").
		int foundF = 0;
		int foundG = 0;
		int foundH = 0;
		for (AnnotationMirror anno : annotationsH) {
			Map<? extends ExecutableElement, ? extends AnnotationValue> values = anno.getElementValues();
			AnnotationValue valueY = values.get(_annoYValue);
			if (null != valueY) {
				if ("on F".equals(valueY.getValue())) {
					foundF++;
				}
				else if ("on H".equals(valueY.getValue())) {
					foundH++;
				}
				else {
					reportError("examineGetAllAnnotations: unexpected value for annotation AnnoY");
					return false;
				}
			}
			else {
				AnnotationValue valueX = values.get(_annoXValue);
				if (null != valueX) {
					if ("on G".equals(valueX.getValue())) {
						foundG++;
					}
					else {
						reportError("examineGetAllAnnotations: unexpected value for annotation AnnoX");
						return false;
					}
				}
				else {
					reportError("examineGetAllAnnotations: getAllAnnotationMirrors(_elementH) returned a mirror with no value()");
					return false;
				}
			}
		}
		if (0 != foundF || 1 != foundG || 1 != foundH) {
			reportError("examineGetAllAnnotations: getAllAnnotationMirrors() found wrong number of annotations on H");
			return false;
		}
		return true;
	}

	/**
	 * Test the {@link Elements#getAllMembers()} method
	 * @return true if all tests passed
	 */
	private boolean examineGetAllMembers()
	{
		List<? extends Element> members = _elementUtils.getAllMembers(_elementG);
		if (null == members) {
			reportError("examineGetAllMembers: getAllMembers(_elementG) returned null");
			return false;
		}
		
		// G member list should contain Object methods, e.g., hashCode()
		boolean foundHashCode = false;
		for (ExecutableElement method : ElementFilter.methodsIn(members)) {
			if ("hashCode".equals(method.getSimpleName().toString())) {
				foundHashCode = true;
				break;
			}
		}
		if (!foundHashCode) {
			reportError("examineGetAllMembers: getAllMembers(_elementG) did not include method hashCode()");
			return false;
		}
		
		// G member list should contain F's nested FChild class
		boolean foundFChild = false;
		for (TypeElement type : ElementFilter.typesIn(members)) {
			if (type.equals(_elementFChild)) {
				foundFChild = true;
				break;
			}
		}
		if (!foundFChild) {
			reportError("examineGetAllMembers: getAllMembers(_elementG) did not include class FChild");
			return false;
		}
		
		// G member list should contain F's _fieldT1_protected
		// G member list should not contain F's _fieldT1_private, because it is hidden
		boolean foundFProtectedField = false;
		for (VariableElement field : ElementFilter.fieldsIn(members)) {
			if ("_fieldT1_protected".equals(field.getSimpleName().toString())) {
				foundFProtectedField = true;
			}
			else if ("_fieldT1_private".equals(field.getSimpleName().toString())) {
				reportError("examineGetAllMembers: getAllMembers(_elementG) included the private inherited field _fieldT1_private");
				return false;
			}
		}
		if (!foundFProtectedField) {
			reportError("examineGetAllMembers: getAllMembers(_elementG) did not return the protected inherited field _fieldT1_protected");
			return false;
		}
		
		// G member list should contain G() constructor
		// G member list should not contain F() constructor
		boolean foundGConstructor = false;
		for (ExecutableElement method : ElementFilter.constructorsIn(members)) {
			Element enclosing = method.getEnclosingElement();
			if (_elementG.equals(enclosing)) {
				foundGConstructor = true;
			}
			else {
				reportError("examineGetAllMembers: getAllMembers(_elementG) returned a constructor for an element other than G");
				return false;
			}
		}
		if (!foundGConstructor) {
			reportError("examineGetAllMembers: getAllMembers(_elementG) did not include G's constructor");
			return false;
		}

		// G member list should contain G's method_T1(String)
		// G member list should not contain F's method_T1(T1), because it is overridden by G
		boolean foundGMethodT1 = false;
		for (ExecutableElement method : ElementFilter.methodsIn(members)) {
			Element enclosing = method.getEnclosingElement();
			if ("method_T1".equals(method.getSimpleName().toString())) {
				if (_elementG.equals(enclosing)) {
					foundGMethodT1 = true;
				}
				else {
					reportError("examineGetAllMembers: getAllMembers(_elementG) included an overridden version of method_T1()");
					return false;
				}
			}
		}
		if (!foundGMethodT1) {
			reportError("examineGetAllMembers: getAllMembers(_elementG) did not include G's method_T1(String)");
			return false;
		}
		return true;
	}

	/**
	 * Test the {@link Elements#isDeprecated()} method
	 * @return true if all tests passed
	 */
	private boolean examineIsDeprecated()
	{
		Element _deprecatedElem = _elementUtils.getTypeElement("targets.model.pc.Deprecation");
		if (null == _deprecatedElem) {
			reportError("examineIsDeprecated: Couldn't find targets.model.pc.Deprecation");
			return false;
		}
		ExecutableElement methodDeprecated = null;
		ExecutableElement methodNotDeprecated = null;
		for (ExecutableElement method : ElementFilter.methodsIn(_deprecatedElem.getEnclosedElements())) {
			if ("deprecatedMethod".equals(method.getSimpleName().toString())) {
				methodDeprecated = method;
			}
			else if ("nonDeprecatedMethod".equals(method.getSimpleName().toString())) {
				methodNotDeprecated = method;
			}
		}
		if (null == methodDeprecated || null == methodNotDeprecated) {
			reportError("examineIsDeprecated: Could not find methods Deprecation.deprecatedMethod() or Deprecation.nonDeprecatedMethod()");
			return false;
		}
		if (_elementUtils.isDeprecated(methodNotDeprecated)) {
			reportError("examineIsDeprecated: ElementUtils.isDeprecated(Deprecation.nonDeprecatedMethod()) is true");
			return false;
		}
		if (!_elementUtils.isDeprecated(methodDeprecated)) {
			reportError("examineIsDeprecated: ElementUtils.isDeprecated(Deprecation.deprecatedMethod()) is false");
			return false;
		}
		TypeElement classDeprecated = null;
		TypeElement classNotDeprecated = null;
		TypeElement interfaceDeprecated = null;
		TypeElement interfaceNotDeprecated = null;
		for (TypeElement type : ElementFilter.typesIn(_deprecatedElem.getEnclosedElements())) {
			if ("deprecatedClass".equals(type.getSimpleName().toString())) {
				classDeprecated = type;
			}
			else if ("nonDeprecatedClass".equals(type.getSimpleName().toString())) {
				classNotDeprecated = type;
			}
			else if ("deprecatedInterface".equals(type.getSimpleName().toString())) {
				interfaceDeprecated = type;
			}
			else if ("nonDeprecatedInterface".equals(type.getSimpleName().toString())) {
				interfaceNotDeprecated = type;
			}
		}
		if (null == classDeprecated || null == classNotDeprecated) {
			reportError("examineIsDeprecated: Could not find methods Deprecation.deprecatedClass() or Deprecation.nonDeprecatedClass()");
			return false;
		}
		if (null == interfaceDeprecated || null == interfaceNotDeprecated) {
			reportError("examineIsDeprecated: Could not find methods Deprecation.deprecatedInterface() or Deprecation.nonDeprecatedInterface()");
			return false;
		}
		if (_elementUtils.isDeprecated(classNotDeprecated)) {
			reportError("examineIsDeprecated: ElementUtils.isDeprecated(Deprecation.nonDeprecatedClass()) is true");
			return false;
		}
		if (!_elementUtils.isDeprecated(classDeprecated)) {
			reportError("examineIsDeprecated: ElementUtils.isDeprecated(Deprecation.deprecatedClass()) is false");
			return false;
		}
		if (_elementUtils.isDeprecated(interfaceNotDeprecated)) {
			reportError("examineIsDeprecated: ElementUtils.isDeprecated(Deprecation.nonDeprecatedInterface()) is true");
			return false;
		}
		if (!_elementUtils.isDeprecated(interfaceDeprecated)) {
			reportError("examineIsDeprecated: ElementUtils.isDeprecated(Deprecation.deprecatedInterface()) is false");
			return false;
		}
		
		TypeElement deprecatedInnerClass = _elementUtils.getTypeElement("targets.model.pc.Deprecation.deprecatedClass");
		if (null == deprecatedInnerClass) {
			reportError("examineIsDeprecated: Couldn't find class Deprecation.deprecatedClass");
			return false;
		}

		return true;
	}

	/**
	 * Test the {@link Elements#getBinaryName(TypeElement)} method
	 * @return true if all tests passed
	 */
	private boolean examineBinaryName() {
		final String refNameF = "targets.model.pc.F";
		final String refBNameFChild = "targets.model.pc.F$FChild";
		final String refBNameFEnum = "targets.model.pc.F$FEnum";
		String bnameF = _elementUtils.getBinaryName(_elementF).toString();
		if (!refNameF.equals(bnameF)) {
			reportError("examineBinaryName: getBinaryName(F) should be " + refNameF + ", was: " + bnameF);
			return false;
		}
		String bnameFChild = _elementUtils.getBinaryName(_elementFChild).toString();
		if (!refBNameFChild.equals(bnameFChild)) {
			reportError("examineBinaryName: getBinaryName(F) should be " + refBNameFChild + ", was: " + bnameF);
			return false;
		}
		String bnameFEnum = _elementUtils.getBinaryName(_elementFEnum).toString();
		if (!refBNameFEnum.equals(bnameFEnum)) {
			reportError("examineBinaryName: getBinaryName(F) should be " + refBNameFEnum + ", was: " + bnameF);
			return false;
		}
		return true;
	}
	
	/**
	 * Test the {@link Elements#getDocComment(TypeElement)} method
	 * @return true if all tests passed
	 */
	private boolean examineGetDocComment() {
		// Javadoc for element F and its enclosed elements - map of element simple name to javadoc
		Map<String, String> nameToDoc = new HashMap<String, String>();
		nameToDoc.put("F", " Javadoc on element F\n @param <T1> a type parameter\n");
		nameToDoc.put("FChild", " Javadoc on nested element FChild\n");
		nameToDoc.put("FEnum", " Javadoc on nested enum FEnum\n Two lines long\n");
		nameToDoc.put("FChildI", 
				" Javadoc on nested interface FChildI\n" +
				"\tthis line has tab after asterisk and ends with another tab\t\n" +
				"\tthis one too\t\n" +
				"   this line has three spaces after asterisk and ends with three spaces   \n" +
				" this line has only one space before the asterisk\n");
		nameToDoc.put("_fieldT1_protected", "Javadoc on field _fieldT1_protected, inline format ");
		nameToDoc.put("_fieldT1_private", 
				"\n" +
				" Javadoc on _fieldT1_private\n" +
				"  this line starts with two spaces, no asterisk\n" +
				"\tThis line starts, contains\tand ends with a tab\t\n" +
				" \t \t This line starts with a space, tab, space, tab, space\n");
		nameToDoc.put("fieldInt", null);
		nameToDoc.put("method_T1", " Javadoc on F.method_T1\n");
		nameToDoc.put("method_String", null);
		nameToDoc.put("getAnonymousObjectAbstract", "");
		nameToDoc.put("add", "\n	@add(int)\n");
		nameToDoc.put("foo", "\n Creates a new instance of AllChecks \n");
		nameToDoc.put("bar", " @bar\t(int)\n");
		nameToDoc.put("bar2", "\t@bar2(int)\n");
		nameToDoc.put("m", "\t\tMethod\tm\n");
		nameToDoc.put("m1", "This is a comment for the method m1,\n" + 
				"  it is on two lines\n");
		nameToDoc.put("m2", "Another comment - starts on first line and\n" + 
				"    continue on the second line ");
		nameToDoc.put("m3", " One more test case that.\n" + 
				" needs\n" + 
				" to be verified.\n" + 
				"\n" + 
				" An empty line with no spaces need to be seen as an empty line.\n" + 
				"\n" + 
				" End of the comment.\n");

		String actual = _elementUtils.getDocComment(_elementF);
		String expected = nameToDoc.get("F");
		if (!expected.equals(actual)) {
			reportError("examineGetDocComment: Unexpected result from getDocComment(F): " + actual);
			return false;
		}
		for (Element e : _elementF.getEnclosedElements()) {
			String name = e.getSimpleName().toString();
			if (nameToDoc.containsKey(name)) {
				actual = _elementUtils.getDocComment(e);
				expected = nameToDoc.get(name);
				if (expected == null && actual != null) {
					reportError("examineGetDocComment: Expected getDocComment(" + name + ") to return null, but got " + _elementUtils.getDocComment(e));
					return false;
				}
				else if (expected != null) {
					if (!expected.equals(actual)) {
						reportError("examineGetDocComment: Unexpected result from getDocComment(" + name + "): " + _elementUtils.getDocComment(e));
						return false;
					}
				}
				
			}
		}
		
		return true;
	}
	
	/**
	 * Test the {@link Elements#hides(Element, Element)} method for fields
	 * @return true if all tests passed
	 */
	private boolean examineHidesField() {
		VariableElement fieldIntJ = null;
		VariableElement fieldIntH = null;
		VariableElement fieldIntG = null;
		VariableElement fieldIntF = null;
		ExecutableElement methodFieldIntJ = null;
		for (VariableElement field : ElementFilter.fieldsIn(_elementF.getEnclosedElements())) {
			if ("fieldInt".equals(field.getSimpleName().toString())) {
				fieldIntF = field;
				break;
			}
		}
		for (VariableElement field : ElementFilter.fieldsIn(_elementG.getEnclosedElements())) {
			if ("fieldInt".equals(field.getSimpleName().toString())) {
				fieldIntG = field;
				break;
			}
		}
		for (VariableElement field : ElementFilter.fieldsIn(_elementH.getEnclosedElements())) {
			if ("fieldInt".equals(field.getSimpleName().toString())) {
				fieldIntH = field;
				break;
			}
		}
		for (VariableElement field : ElementFilter.fieldsIn(_elementJ.getEnclosedElements())) {
			if ("fieldInt".equals(field.getSimpleName().toString())) {
				fieldIntJ = field;
				break;
			}
		}
		for (ExecutableElement method : ElementFilter.methodsIn(_elementJ.getEnclosedElements())) {
			if ("fieldInt".equals(method.getSimpleName().toString())) {
				methodFieldIntJ = method;
				break;
			}
		}
		if (null == fieldIntJ || null == fieldIntH || null == fieldIntG || null == fieldIntF) {
			reportError("examineHidesField: Failed to find field \"fieldInt\" in either F, G, H, or J");
			return false;
		}
		if (null == methodFieldIntJ) {
			reportError("examineHidesField: Failed to find method \"fieldInt()\" in J");
			return false;
		}
		// Should hide:
		if (!_elementUtils.hides(fieldIntH, fieldIntF)) {
			reportError("examineHidesField: H.fieldInt should hide F.fieldInt");
			return false;
		}
		// Should not hide:
		if (_elementUtils.hides(fieldIntF, fieldIntF)) {
			reportError("examineHidesField: F.fieldInt should not hide itself");
			return false;
		}
		if (_elementUtils.hides(fieldIntF, fieldIntG)) {
			reportError("examineHidesField: F.fieldInt should not hide G.fieldInt");
			return false;
		}
		if (!_elementUtils.hides(fieldIntG, fieldIntF)) {
			reportError("examineHidesField: G.fieldInt should hide F.fieldInt");
			return false;
		}
		if (_elementUtils.hides(fieldIntJ, fieldIntG)) {
			reportError("examineHidesField: J.fieldInt should not hide G.fieldInt");
			return false;
		}
		if (_elementUtils.hides(fieldIntJ, methodFieldIntJ)) {
			reportError("examineHidesField: field J.fieldInt should not hide method J.fieldInt()");
			return false;
		}
		return true;
	}
	
	/**
	 * Test the {@link Elements#getConstantExpression(Object)} method for fields
	 * @return true if all tests passed
	 */
	private boolean examineGetConstantExpression() {
		for (VariableElement field : ElementFilter.fieldsIn(_elementConstants.getEnclosedElements())) {
			Object constantValue = field.getConstantValue();
			if (constantValue instanceof String) {
				String constantExpression = _elementUtils.getConstantExpression(constantValue);
				if (constantExpression == null
						|| constantExpression.charAt(0) != '\"'
						|| constantExpression.charAt(constantExpression.length() - 1) != '\"') {
					reportError("Unexpected value for String: " + String.valueOf(constantExpression));
					return false;
				}
			} else if (constantValue instanceof Character) {
				String constantExpression = _elementUtils.getConstantExpression(constantValue);
				if (constantExpression == null
						|| constantExpression.charAt(0) != '\''
						|| constantExpression.charAt(constantExpression.length() - 1) != '\'') {
					reportError("Unexpected value for Character: " + String.valueOf(constantExpression));
					return false;
				}
			} else {
				String constantExpression = _elementUtils.getConstantExpression(constantValue);
				if (constantExpression == null) {
					reportError("Constant expression is null");
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Test the {@link Elements#hides(Element, Element)} method for nested classes
	 * @return true if all tests passed
	 */
	private boolean examineHidesClass() {
		TypeElement elementFChildOnF = null;
		TypeElement elementFChildOnH = null;
		TypeElement elementFOnJ = null;
		TypeElement elementFChildOnJ = null;
		TypeElement elementIFChildOnIF = null;
		TypeElement elementIFChildOnH = null;
		TypeElement elementIF = _elementUtils.getTypeElement("targets.model.pc.IF");
		for (TypeElement element : ElementFilter.typesIn(elementIF.getEnclosedElements())) {
			String name = element.getSimpleName().toString();
			if ("IFChild".equals(name)) {
				elementIFChildOnIF = element;
				break;
			}
		}
		for (TypeElement element : ElementFilter.typesIn(_elementF.getEnclosedElements())) {
			String name = element.getSimpleName().toString();
			if ("FChild".equals(name)) {
				elementFChildOnF = element;
				break;
			}
		}
		for (TypeElement element : ElementFilter.typesIn(_elementH.getEnclosedElements())) {
			String name = element.getSimpleName().toString();
			if ("FChild".equals(name)) {
				elementFChildOnH = element;
			}
			else if ("IFChild".equals(name)) {
				elementIFChildOnH = element;
			}
		}
		for (TypeElement element : ElementFilter.typesIn(_elementJ.getEnclosedElements())) {
			String name = element.getSimpleName().toString();
			if ("FChild".equals(name)) {
				elementFChildOnJ = element;
			}
			else if ("F".equals(name)) {
				elementFOnJ = element;
			}
		}
		Element elementFPackage = _elementF.getEnclosingElement();
		
		// Should hide:
		if (!_elementUtils.hides(elementFChildOnH, elementFChildOnF)) {
			reportError("examineHidesClass: H.FChild should hide F.FChild");
			return false;
		}
		if (!_elementUtils.hides(elementIFChildOnH, elementIFChildOnIF)) {
			reportError("examineHidesClass: H.IFChild should hide IF.IFChild");
			return false;
		}
		// Should not hide:
		if (_elementUtils.hides(elementFChildOnF, elementFChildOnF)) {
			reportError("examineHidesClass: F.FChild should not hide itself");
			return false;
		}
		if (_elementUtils.hides(elementIFChildOnH, elementFChildOnF)) {
			reportError("examineHidesClass: H.IFChild should not hide F.FChild");
			return false;
		}
		if (_elementUtils.hides(elementFChildOnF, elementFChildOnH)) {
			reportError("examineHidesClass: F.FChild should not hide H.FChild");
			return false;
		}
		if (_elementUtils.hides(elementFChildOnJ, elementFChildOnF)) {
			reportError("examineHidesClass: J.FChild should not hide F.FChild");
			return false;
		}
		if (_elementUtils.hides(_elementF, elementFOnJ)) {
			reportError("examineHidesClass: J.F should not hide F");
			return false;
		}
		if (_elementUtils.hides(_elementF, elementFPackage) || _elementUtils.hides(elementFPackage, _elementF)) {
			reportError("examineHidesClass: F should not hide its enclosing package, and vice versa");
			return false;
		}
		return true;
	}

	/**
	 * Test the {@link Elements#hides(Element, Element)} method for methods
	 * @return true if all tests passed
	 */
	private boolean examineHidesMethod() {
		ExecutableElement methodStaticOnF = null;
		ExecutableElement methodStatic2OnF = null;
		ExecutableElement methodT1OnF = null;
		ExecutableElement methodStaticOnG = null;
		ExecutableElement methodT1OnG = null;
		ExecutableElement methodStaticOnH = null;
		ExecutableElement methodStaticIntOnH = null;
		ExecutableElement methodStaticOnJ = null;
		for (ExecutableElement method : ElementFilter.methodsIn(_elementF.getEnclosedElements())) {
			String name = method.getSimpleName().toString();
			if ("staticMethod".equals(name)) {
				methodStaticOnF = method;
			}
			else if ("staticMethod2".equals(name)) {
				methodStatic2OnF = method;
			}
			else if ("method_T1".equals(name)) {
				methodT1OnF = method;
			}
		}
		for (ExecutableElement method : ElementFilter.methodsIn(_elementG.getEnclosedElements())) {
			String name = method.getSimpleName().toString();
			if ("staticMethod".equals(name)) {
				methodStaticOnG = method;
			}
			else if ("method_T1".equals(name)) {
				methodT1OnG = method;
			}
		}
		for (ExecutableElement method : ElementFilter.methodsIn(_elementH.getEnclosedElements())) {
			String name = method.getSimpleName().toString();
			if ("staticMethod".equals(name)) {
				if (method.getParameters().isEmpty()) {
					methodStaticOnH = method;
				}
				else {
					methodStaticIntOnH = method;
				}
			}
		}
		for (ExecutableElement method : ElementFilter.methodsIn(_elementJ.getEnclosedElements())) {
			String name = method.getSimpleName().toString();
			if ("staticMethod".equals(name)) {
				methodStaticOnJ = method;
				break;
			}
		}
		if (methodStaticOnF == null || methodStatic2OnF == null || methodT1OnF == null) {
			reportError("examineHidesMethod: Failed to find an expected method on F");
			return false;
		}
		if (methodStaticOnG == null || methodT1OnG == null) {
			reportError("examineHidesMethod: Failed to find an expected method on G");
			return false;
		}
		if (methodStaticOnH == null || methodStaticIntOnH == null) {
			reportError("examineHidesMethod: Failed to find an expected method on H");
			return false;
		}
		if (methodStaticOnJ == null) {
			reportError("examineHidesMethod: Failed to find an expected method on J");
			return false;
		}
		
		// The should-hide cases
		if (!_elementUtils.hides(methodStaticOnH, methodStaticOnG)) {
			reportError("examineHidesMethod: H.staticMethod() should hide G.staticMethod()");
			return false;
		}
		
		// The should-not-hide cases
		if (_elementUtils.hides(methodStaticOnG, methodStaticOnG)) {
			reportError("examineHidesMethod: G.staticMethod() should not hide itself");
			return false;
		}
		if (_elementUtils.hides(methodStaticOnG, methodStaticOnF)) {
			reportError("examineHidesMethod: G.staticMethod() should not hide (private) F.staticMethod()");
			return false;
		}
		if (_elementUtils.hides(methodStaticOnG, methodStaticOnH)) {
			reportError("examineHidesMethod: G.staticMethod() should not hide H.staticMethod()");
			return false;
		}
		if (_elementUtils.hides(methodStaticOnG, methodStatic2OnF)) {
			reportError("examineHidesMethod: G.staticMethod() should not hide F.staticMethod2()");
			return false;
		}
		if (_elementUtils.hides(methodStaticOnJ, methodStaticOnG)) {
			reportError("examineHidesMethod: J.staticMethod() should not hide G.staticMethod()");
			return false;
		}
		if (_elementUtils.hides(methodStaticIntOnH, methodStaticOnG)) {
			reportError("examineHidesMethod: H.staticMethod(int) should not hide G.staticMethod()");
			return false;
		}
		if (_elementUtils.hides(methodT1OnG, methodT1OnF)) {
			reportError("examineHidesMethod: G.methodT1() should not hide F.methodT1(), because they aren't static (JLS 8.4.8.2)");
			return false;
		}
		return true;
	}
	
	/**
	 * Test the {@link Elements#overrides(ExecutableElement, ExecutableElement, TypeElement)} implementation
	 * @return true if all tests passed
	 */
	private boolean examineOverrides() {
		// D extends (C extends A implements B).  X is unrelated.
		TypeElement typeA = _elementUtils.getTypeElement("targets.model.pc.Overriding.A");
		TypeElement typeB = _elementUtils.getTypeElement("targets.model.pc.Overriding.B");
		TypeElement typeC = _elementUtils.getTypeElement("targets.model.pc.Overriding.C");
		TypeElement typeD = _elementUtils.getTypeElement("targets.model.pc.Overriding.D");
		TypeElement typeX = _elementUtils.getTypeElement("targets.model.pc.F");
		if (typeA == null || typeB == null || typeC == null || typeD == null) {
			reportError("Unable to find types in targets.model.pc.Overriding");
			return false;
		}
		ExecutableElement methodAF = null;
		ExecutableElement methodAG = null;
		ExecutableElement methodAH = null;
		ExecutableElement methodAJ = null;
		ExecutableElement methodBF = null;
		ExecutableElement methodBG = null;
		ExecutableElement methodBH = null;
		ExecutableElement methodCH = null;
		ExecutableElement methodDF = null;
		ExecutableElement methodDG = null;
		ExecutableElement methodDJ = null;
		ExecutableElement methodXF = null;
		for (ExecutableElement method : ElementFilter.methodsIn(typeA.getEnclosedElements())) {
			String name = method.getSimpleName().toString();
			if ("f".equals(name)) {
				methodAF = method;
			}
			else if ("g".equals(name)) {
				methodAG = method;
			}
			else if ("h".equals(name)) {
				methodAH = method;
			}
			else if ("j".equals(name)) {
				methodAJ = method;
			}
		}
		for (ExecutableElement method : ElementFilter.methodsIn(typeB.getEnclosedElements())) {
			String name = method.getSimpleName().toString();
			if ("f".equals(name)) {
				methodBF = method;
			}
			else if ("g".equals(name)) {
				methodBG = method;
			}
			else if ("h".equals(name)) {
				methodBH = method;
			}
		}
		for (ExecutableElement method : ElementFilter.methodsIn(typeC.getEnclosedElements())) {
			String name = method.getSimpleName().toString();
			if ("h".equals(name)) {
				methodCH = method;
				break;
			}
		}
		for (ExecutableElement method : ElementFilter.methodsIn(typeD.getEnclosedElements())) {
			String name = method.getSimpleName().toString();
			if ("f".equals(name)) {
				methodDF = method;
			}
			else if ("g".equals(name)) {
				methodDG = method;
			}
			else if ("j".equals(name)) {
				methodDJ = method;
			}
		}
		for (ExecutableElement method : ElementFilter.methodsIn(typeX.getEnclosedElements())) {
			String name = method.getSimpleName().toString();
			if ("f".equals(name)) {
				methodXF = method;
				break;
			}
		}
		if (null == methodAF || null == methodAG || null == methodAH || null == methodAJ ||
				null == methodBF || null == methodBG || null == methodBH ||
				null == methodCH ||
				null == methodDF || null == methodDG || null == methodDJ ||
				null == methodXF) {
			reportError("examineOverrides: could not find some methods");
			return false;
		}
		
		// Should override:
		if (!_elementUtils.overrides(methodAF, methodBF, typeC)) {
			reportError("examineOverrides: A.f() should override B.f() in the context of C");
			return false;
		}
		if (!_elementUtils.overrides(methodCH, methodAH, typeC)) {
			reportError("examineOverrides: C.h() should override A.h() in the context of C");
			return false;
		}
		if (!_elementUtils.overrides(methodCH, methodAH, typeD)) {
			reportError("examineOverrides: C.h() should override A.h() in the context of D");
			return false;
		}
		if (!_elementUtils.overrides(methodDF, methodBF, typeD)) {
			reportError("examineOverrides: D.f() should override B.f() in the context of D");
			return false;
		}
		if (!_elementUtils.overrides(methodDG, methodBG, typeD)) {
			reportError("examineOverrides: D.g() should override B.g() in the context of D");
			return false;
		}
		if (!_elementUtils.overrides(methodDJ, methodAJ, typeD)) {
			reportError("examineOverrides: D.j() should override A.j() in the context of D");
			return false;
		}
		if (!_elementUtils.overrides(methodAH, methodBH, typeC)) {
			reportError("examineOverrides: A.h() should override B.h() in the context of C (even though C.h does too)");
			return false;
		}
		
		// Should not override:
		if (_elementUtils.overrides(methodAF, methodAF, typeA)) {
			reportError("examineOverrides: A.f() should not override itself in the context of A");
			return false;
		}
		if (_elementUtils.overrides(methodAF, methodAF, typeC)) {
			reportError("examineOverrides: A.f() should not override itself in the context of C");
			return false;
		}
		if (_elementUtils.overrides(methodAF, methodBF, typeA)) {
			reportError("examineOverrides: A.f() should not override B.f() in the context of A");
			return false;
		}
		if (_elementUtils.overrides(methodAG, methodBG, typeC)) {
			reportError("examineOverrides: private A.g() should not override B.g() in the context of C");
			return false;
		}
		if (_elementUtils.overrides(methodDG, methodAG, typeD)) {
			reportError("examineOverrides: D.g() should not override private A.g() in the context of D");
			return false;
		}
		if (_elementUtils.overrides(methodXF, methodAF, typeD)) {
			reportError("examineOverrides: unrelated X.f() should not override A.f() in the context of D");
			return false;
		}
		if (_elementUtils.overrides(methodXF, methodBF, typeX)) {
			reportError("examineOverrides: X.f() should not override unrelated B.f() in the context of X");
			return false;
		}
		
		// These cases seem like they should return false, but javac returns true:
		if (!_elementUtils.overrides(methodDJ, methodAJ, typeC)) {
			reportError("examineOverrides: to match javac, D.j() should override A.j() in the context of C");
			return false;
		}
		if (!_elementUtils.overrides(methodDF, methodAF, typeC)) {
			reportError("examineOverrides: to match javac, D.f() should override A.f() in the context of C");
			return false;
		}
		if (!_elementUtils.overrides(methodDF, methodBF, typeC)) {
			reportError("examineOverrides: to match javac, D.f() should override B.f() in the context of C");
			return false;
		}
		
		return true;
	}

}
