/*******************************************************************************
 * Copyright (c) 2007, 2015 BEA 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:
 *    wharley@bea.com - initial API and implementation
 *    akurtakov@gmail.com - fix compilation with Java 7
 *    
 *******************************************************************************/

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
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.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.MirroredTypesException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;

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

/**
 * A processor that explores the "model" target hierarchy and complains if it does
 * not find what it expects.  To enable this processor, add 
 * -Aorg.eclipse.jdt.compiler.apt.tests.processors.elements.ElementProc to the command line.
 * @since 3.3
 */
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class ElementProc extends BaseProcessor {
	
	// The set of elements we expect getRootElements to return in package pa
	private static final String[] ROOT_ELEMENT_NAMES = new String[] {
		"targets.model.pa.AnnoZ", "targets.model.pa.A", "targets.model.pa.IA", "targets.model.pa.ExceptionA"};
	
	// Initialized in collectElements()
	private TypeElement _elementIA;
	private TypeElement _elementAB;
	private TypeElement _elementA;
	private TypeElement _elementD;
	private TypeElement _elementDChild;
	private TypeElement _elementAnnoZ;
	private TypeElement _elementString;

	// Initialized in examineDMethods()
	private ExecutableElement _methodDvoid;
	private ExecutableElement _methodDvoid2;
	private ExecutableElement _methodDvoid3;
	private TypeElement _elementDEnum;
	
	// 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 (!examineRoundEnv(roundEnv)) {
			return false;
		}
		
		if (!examineABInterfaces()) {
			return false;
		}
		
		if (!examineABModifiers()) {
			return false;
		}
		
		if (!examineDHierarchy()) {
			return false;
		}
		
		if (!examineAMethodsAndFields()) {
			return false;
		}
		
		if (!examineAMethodThrowables()) {
			return false;
		}
		
		if (!examineDMethods()) {
			return false;
		}
		
		if (!examineDEnum()) {
			return false;
		}
		
		if (!examinePBPackage()) {
			return false;
		}
		
		if (!examineDAnnotations()) {
			return false;
		}
		
		if (!examineGetAnnotation()) {
			return false;
		}
		
		if (!bug261969()) {
			return false;
		}
		
		if(!bug300408()) {
			return false;
		}

		if (!bug467928_enumFields()) {
			return false;
		}

		reportSuccess();
		return false;
	}

	private boolean bug467928_enumFields() {
		TypeElement type =  _elementUtils.getTypeElement( "targets.model.enumfields.EnumWithFields");
		Map<String, VariableElement> fields = new HashMap<>();
		for (Element element : type.getEnclosedElements()) {
			if (element instanceof VariableElement) {
				fields.put(element.getSimpleName().toString(), (VariableElement) element);
			} else if (element instanceof ExecutableElement) {
				ExecutableElement method = (ExecutableElement) element;
				if (method.getSimpleName().toString().equals("setField")) {
					fields.put("param", method.getParameters().get(0));
				}
			}
		}

		if (fields.get("CONST").getKind() != ElementKind.ENUM_CONSTANT) {
			return false;
		}

		if (fields.get("field").getKind() != ElementKind.FIELD) {
			return false;
		}
		if (fields.get("param").getKind() != ElementKind.PARAMETER) {
			return false;
		}

		return true;
	}

	/**
	 * Regression test for Bug 300408, checking if TypeElement.getEnclosedTypes() returns the elements
	 * in the order as declared in the source
	 * 
	 * @return true if all tests passed
	 */
	private boolean bug300408() {
		if (!enclosedElementOrderCorrect(
				"targets.model.order.Ordered",
				Arrays.asList(ElementKind.CONSTRUCTOR),
				"methodB", "field1", "methodA")) {

			return false;
		}
		
		if (!enclosedElementOrderCorrect("targets.model.order.EnumInstances",
				Arrays.asList(ElementKind.CONSTRUCTOR, ElementKind.METHOD),
				"A", "G", "U", "D", "I", "N")) {

			return false;
		}

		return true;
	}

	private boolean enclosedElementOrderCorrect(String className, Collection<ElementKind> ignoredKinds, String ... expectedOrder) {
		TypeElement elementOrdered = _elementUtils.getTypeElement(className);
		List<? extends Element> enclosedElements = elementOrdered.getEnclosedElements();

		List<String> elementNames = new ArrayList<String>(enclosedElements.size());
		for (Element e : enclosedElements) {
			if (!ignoredKinds.contains(e.getKind())) {
				elementNames.add(e.getSimpleName().toString());
			}
		}

		List<String> expected = Arrays.asList(expectedOrder);

		if (!elementNames.equals(expected)) {
			reportError("Unexpected order of elements in class " + className + ". Expected: " + expected + ", but got: " + elementNames);
			return false;
		}

		return true;
	}

	/**
	 * Collect some elements that will be reused in various tests
	 * @return true if all tests passed
	 */
	private boolean collectElements() {
		_elementIA = _elementUtils.getTypeElement("targets.model.pa.IA");
		if (_elementIA == null) {
			reportError("element IA was not found");
			return false;
		}
		if (_elementIA.getKind() != ElementKind.INTERFACE) {
			reportError("IA claims to not be an interface");
			return false;
		}
		if (_elementIA.getNestingKind() != NestingKind.TOP_LEVEL) {
			reportError("NestingKind of element IA is not TOP_LEVEL");
			return false;
		}
		
		_elementA = _elementUtils.getTypeElement("targets.model.pa.A");
		if (_elementA == null) {
			reportError("element A was not found");
			return false;
		}
		if (_elementA.getKind() != ElementKind.CLASS) {
			reportError("A claims to not be a class");
			return false;
		}
		
		_elementAnnoZ = _elementUtils.getTypeElement("targets.model.pa.AnnoZ");
		if (_elementAnnoZ == null) {
			reportError("element AnnoZ was not found");
			return false;
		}
		if (_elementAnnoZ.getKind() != ElementKind.ANNOTATION_TYPE) {
			reportError("AnnoZ claims to not be an annotation type");
			return false;
		}
		
		_elementAB = _elementUtils.getTypeElement("targets.model.pb.AB");
		if (_elementAB == null) {
			reportError("element AB was not found");
			return false;
		}
		if (_elementAB.getKind() != ElementKind.CLASS) {
			reportError("AB claims to not be a class");
			return false;
		}
		
		_elementD = _elementUtils.getTypeElement("targets.model.pb.D");
		if (_elementD == null) {
			reportError("element D was not found");
			return false;
		}
		if (_elementD.getKind() != ElementKind.CLASS) {
			reportError("D claims to not be a class");
			return false;
		}
		
		_elementDChild = _elementUtils.getTypeElement("targets.model.pb.DChild");
		if (_elementDChild == null) {
			reportError("secondary element DChild was not found");
			return false;
		}
		if (_elementDChild.getKind() != ElementKind.CLASS) {
			reportError("DChild claims to not be a class");
			return false;
		}
		_elementString = _elementUtils.getTypeElement("java.lang.String");
		return true;
	}
	
	/**
	 * Check the methods on RoundEnvironment method
	 * @return true if all tests passed
	 */
	private boolean examineRoundEnv(RoundEnvironment roundEnv) {
		// Verify that we get the root elements we expect
		Set<String> expectedRootElementNames = new HashSet<String>(ROOT_ELEMENT_NAMES.length);
		for (String name : ROOT_ELEMENT_NAMES) {
			expectedRootElementNames.add(name);
		}
		Set<? extends Element> actualRootElements = roundEnv.getRootElements();
		if (null == actualRootElements) {
			reportError("getRootElements() returned null");
			return false;
		}
		for (Element e : actualRootElements) {
			if (e instanceof TypeElement) {
				String name = ((TypeElement)e).getQualifiedName().toString();
				if (name.startsWith("targets.model.pa.") && !expectedRootElementNames.remove(name)) {
					reportError("Missing root element " + name);
					return false;
				}
			}
		}
		if (!expectedRootElementNames.isEmpty()) {
			reportError("Found extra root elements including " + expectedRootElementNames.iterator().next());
			return false;
		}
		
		// Verify that we get the annotations we expect
		Set<? extends Element> annotatedWithAnnoZ = roundEnv.getElementsAnnotatedWith(_elementAnnoZ);
		if (null == annotatedWithAnnoZ || !annotatedWithAnnoZ.contains(_elementD)) {
			reportError("Elements annotated with AnnoZ does not include D");
			return false;
		}
		
		// targets.model.pc.Deprecation contains @Deprecated annotations
		Set<? extends Element> annotatedWithDeprecated = roundEnv.getElementsAnnotatedWith(Deprecated.class);
		if (null == annotatedWithDeprecated) {
			reportError("getElementsAnnotatedWith(@Deprecated) returned null");
			return false;
		}
		boolean foundDeprecation = false;
		for (TypeElement deprecatedElement : ElementFilter.typesIn(annotatedWithDeprecated)) {
			if ("targets.model.pc.Deprecation".equals(deprecatedElement.getQualifiedName().toString())) {
				foundDeprecation = true;
				break;
			}
		}
		if (!foundDeprecation) {
			reportError("getElementsAnnotatedWith(@Deprecation) did not find targets.model.pc.Deprecation");
		}

		return true;
	}
	
	/**
	 * Examine the interfaces that AB implements
	 * @return true if all tests passed
	 */
	private boolean examineABInterfaces() {
		List<? extends TypeMirror> interfacesAB = _elementAB.getInterfaces();
		if (null == interfacesAB) {
			reportError("AB.getInterfaces() returned null");
			return false;
		}
		boolean foundIAinterface = false;
		for (TypeMirror type : interfacesAB) {
			Element decl = _typeUtils.asElement(type);
			if (null == decl) {
				reportError("One of AB's interfaces, " + type.toString() + ", produced null from Types.asElement()");
				return false;
			}
			if (_elementIA.equals(decl)) {
				foundIAinterface = true;
				break;
			}
		}
		if (!foundIAinterface) {
			reportError("AB does not have IA as an interface");
			return false;
		}
		return true;
	}

	/**
	 * Examine the modifiers of AB's contents
	 * @return true if all tests passed
	 */
	private boolean examineABModifiers() {
		Map<String, Element> contents = new HashMap<String, Element>();
		for (Element enclosed : _elementAB.getEnclosedElements()) {
			contents.put(enclosed.getSimpleName().toString(), enclosed);
		}
		Element publicMethod = contents.get("methodIAString");
		Element protectedField = contents.get("_fieldListIA");
		Element privateClass = contents.get("E");
		if (null == publicMethod || null == protectedField || null == privateClass) {
			reportError("AB does not contain the expected enclosed elements");
			return false;
		}
		Set<Modifier> modifiers = publicMethod.getModifiers();
		if (!modifiers.contains(Modifier.PUBLIC) || modifiers.size() > 1) {
			reportError("AB.methodIAString() has unexpected modifiers");
			return false;
		}
		modifiers = protectedField.getModifiers();
		if (!modifiers.contains(Modifier.PROTECTED) || modifiers.size() > 1) {
			reportError("AB._fieldListIA() has unexpected modifiers");
			return false;
		}
		modifiers = privateClass.getModifiers();
		if (!modifiers.contains(Modifier.PRIVATE) || modifiers.size() > 1) {
			reportError("AB.E() has unexpected modifiers");
			return false;
		}
		return true;
	}

	/**
	 * Examine the hierarchy of element D
	 * @return true if all tests passed
	 */
	private boolean examineDHierarchy() {
		TypeMirror supertypeD = _elementD.getSuperclass();
		if (null == supertypeD) {
			reportError("element D's supertype was null");
			return false;
		}
		Element superclassD = _typeUtils.asElement(supertypeD);
		if (!_elementAB.equals(superclassD)) {
			reportError("element D's superclass did not equal element AB");
			return false;
		}

		return true;
	}
	
	/**
	 * Examine the methods and fields of element A
	 * @return true if all tests passed
	 */
	private boolean examineAMethodsAndFields() {
		// METHODS
		List<? extends Element> enclosedA = _elementA.getEnclosedElements();
		if (enclosedA == null) {
			reportError("elementA.getEnclosedElements() returned null");
			return false;
		}
		List<ExecutableElement> methodsA = ElementFilter.methodsIn(enclosedA);
		ExecutableElement methodIAString = null;
		for (ExecutableElement method : methodsA) {
			Name methodName = method.getSimpleName();
			if ("methodIAString".equals(methodName.toString())) {
				methodIAString = method;
			}
		}
		if (null == methodIAString) {
			reportError("element A did not contain methodIAString()");
			return false;
		}
		if (methodIAString.getKind() != ElementKind.METHOD) {
			reportError("A.methodIAString is not an ElementKind.METHOD");
			return false;
		}
		Element enclosingMethodIAStringInA = methodIAString.getEnclosingElement();
		if (null == enclosingMethodIAStringInA || !_elementA.equals(enclosingMethodIAStringInA)) {
			reportError("Element enclosing A.methodIAString() is not A");
			return false;
		}
		
		// RETURN AND PARAMS
		TypeMirror returnType = methodIAString.getReturnType();
		if (!(returnType instanceof DeclaredType) || returnType.getKind() != TypeKind.DECLARED) {
			reportError("Return type of A.methodIAString() is not a declared type");
			return false;
		}
		if (!_elementString.equals(((DeclaredType)returnType).asElement())) {
			reportError("Return type of A.methodIAString() does not equal java.lang.String");
			return false;
		}
		List<ExecutableElement> methodsD = ElementFilter.methodsIn(_elementString.getEnclosedElements());
		for (ExecutableElement method : methodsD) {
			List<? extends VariableElement> params = method.getParameters();
			for (VariableElement param : params) {
				Element enclosingElement = param.getEnclosingElement();
				if (enclosingElement == null) {
					reportError("Enclosing element of a parameter in one of the java.lang.String methods is null");
					return false;
				}
				if (!enclosingElement.equals(method)) {
					reportError("Enclosing element of a parameter in one of the java.lang.String methods is not the method itself");
					return false;
				}
			}
		}
		List<? extends VariableElement> paramsMethodIAString = methodIAString.getParameters();
		VariableElement int1 = null;
		for (VariableElement param : paramsMethodIAString) {
			int1 = param;
		}
		TypeMirror int1Type = int1.asType();
		if (null == int1Type || int1Type.getKind() != TypeKind.INT) {
			reportError("The first parameter of A.methodIAString() is not of int type");
			return false;
		}
		if (!("int1".equals(int1.getSimpleName().toString()))) {
			reportError("The first parameter of A.methodIAString() is not named int1");
			return false;
		}
		
		// FIELDS
		List<VariableElement> fieldsA = ElementFilter.fieldsIn(enclosedA);
		VariableElement fieldAint = null;
		for (VariableElement field : fieldsA) {
			Name fieldName = field.getSimpleName();
			if ("_fieldAint".equals(fieldName.toString())) {
				fieldAint = field;
			}
		}
		if (null == fieldAint) {
			reportError("element A did not contain _fieldAint");
			return false;
		}
		if (fieldAint.getKind() != ElementKind.FIELD) {
			reportError("A._fieldAint is not an ElementKind.FIELD");
			return false;
		}
		return true;
	}
	
	/**
	 * Examine the methods of A which have throws clauses
	 * @return true if all tests passed
	 */
	private boolean examineAMethodThrowables() {
		List<ExecutableElement> methodsA = ElementFilter.methodsIn(_elementA.getEnclosedElements());
		ExecutableElement methodIAString = null; // no throws clauses
		ExecutableElement methodThrows1 = null;
		ExecutableElement methodThrows2 = null;
		for (ExecutableElement method : methodsA) {
			String methodName = method.getSimpleName().toString();
			if ("methodIAString".equals(methodName)) {
				methodIAString = method;
			}
			if ("methodThrows1".equals(methodName)) {
				methodThrows1 = method;
			}
			else if ("methodThrows2".equals(methodName)) {
				methodThrows2 = method;
			}
		}
		if (null == methodIAString || null == methodThrows1 || null == methodThrows2) {
			reportError("element A did not contain methodIAString(), methodThrows1(), or methodThrows2()");
			return false;
		}
		List<? extends TypeMirror> thrownTypes0 = methodIAString.getThrownTypes();
		List<? extends TypeMirror> thrownTypes1 = methodThrows1.getThrownTypes();
		List<? extends TypeMirror> thrownTypes2 = methodThrows2.getThrownTypes();
		if (null == thrownTypes0 || null == thrownTypes1 || null == thrownTypes2) {
			reportError("getThrownTypes() on A.methodIAString(), methodThrows1(), or methodThrows2() returned null");
			return false;
		}
		if (!thrownTypes0.isEmpty()) {
			reportError("A.methodIAString unexpectedly reports having a throws clause");
			return false;
		}
		boolean foundEA = false;
		for (TypeMirror type : thrownTypes1) {
			Element element = _typeUtils.asElement(type);
			if ("ExceptionA".equals(element.getSimpleName().toString())) {
				foundEA = true;
			}
		}
		if (thrownTypes1.size() != 1 || !foundEA) {
			reportError("A.methodThrows1() reported unexpected throwables");
			return false;
		}
		foundEA = false;
		boolean foundUOE = false;
		for (TypeMirror type : thrownTypes2) {
			Element element = _typeUtils.asElement(type);
			if ("UnsupportedOperationException".equals(element.getSimpleName().toString())) {
				foundUOE = true;
			}
			else if ("ExceptionA".equals(element.getSimpleName().toString())) {
				foundEA = true;
			}
		}
		if (thrownTypes2.size() != 2 || !foundEA || !foundUOE) {
			reportError("A.methodThrows2() reported unexpected throwables");
			return false;
		}
		return true;
	}
	
	/**
	 * Examine the methods of D (which are interesting because of an enum param and void return)
	 * @return true if all tests passed
	 */
	private boolean examineDMethods() {
		List<ExecutableElement> methodsD = ElementFilter.methodsIn(_elementD.getEnclosedElements());
		_methodDvoid = null;
		_methodDvoid2 = null;
		_methodDvoid3 = null;
		for (ExecutableElement method : methodsD) {
			Name methodName = method.getSimpleName();
			if ("methodDvoid".equals(methodName.toString())) {
				_methodDvoid = method;
			}
			if ("methodDvoid2".equals(methodName.toString())) {
				_methodDvoid2 = method;
			}
			if ("methodDvoid3".equals(methodName.toString())) {
				_methodDvoid3 = method;
			}
		}
		if (null == _methodDvoid) {
			reportError("element D did not contain methodDvoid()");
			return false;
		}
		TypeMirror returnType = _methodDvoid.getReturnType();
		if (returnType.getKind() != TypeKind.VOID) {
			reportError("D.methodDvoid() return type was not void");
			return false;
		}
		List<? extends VariableElement> params = _methodDvoid.getParameters();
		if (null == params || params.isEmpty()) {
			reportError("D.methodDvoid() reports no parameters");
			return false;
		}
		VariableElement param1 = params.iterator().next();
		TypeMirror param1Type = param1.asType();
		if (null == param1Type || param1Type.getKind() != TypeKind.DECLARED) {
			reportError("First parameter of D.methodDvoid() is not a declared type");
			return false;
		}
		Element enclosingElement = param1.getEnclosingElement();
		if (enclosingElement == null || enclosingElement.getKind() != ElementKind.METHOD) {
			reportError("Enclosing element of first parameter of D.methodDvoid() is null or not a method");
			return false;
		}
		if (!"targets.model.pb.D.DEnum".equals(param1Type.toString())) {
			reportError("Type of first parameter of D.methodDvoid() is not DEnum");
			return false;
		}
		Element param1TypeElement = ((DeclaredType)param1Type).asElement();
		if (null == param1TypeElement || param1TypeElement.getKind() != ElementKind.ENUM || !(param1TypeElement instanceof TypeElement)) {
			reportError("Type of first parameter of D.methodDvoid() is not an enum");
			return false;
		}
		_elementDEnum = (TypeElement)param1TypeElement;
		return true;
	}

	/**
	 * Check the DEnum type declared inside element D
	 * @return true if all tests passed
	 */
	private boolean examineDEnum()
	{
		if (_elementDEnum.getNestingKind() != NestingKind.MEMBER) {
			reportError("Type DEnum is not NestingKind.MEMBER");
			return false;
		}
		Map<String, VariableElement> values = new LinkedHashMap<String, VariableElement>();
		for (VariableElement enclosedElement : ElementFilter.fieldsIn(_elementDEnum.getEnclosedElements())) {
			values.put(enclosedElement.getSimpleName().toString(), enclosedElement);
		}
		if (values.size() != 3) {
			reportError("DEnum should have three values, but instead has: " + values.size());
			return false;
		}
		Iterator<String> iter = values.keySet().iterator();
		if (!"DEnum1".equals(iter.next()) || !"DEnum2".equals(iter.next()) || !"DEnum3".equals(iter.next())) {
			reportError("DEnum does not have the expected values in the expected order");
			return false;
		}
		return true;
	}

	/**
	 * Check the PackageDeclaration of pb
	 * @return true if all tests passed
	 */
	private boolean examinePBPackage() {
		Element packagePB = _elementAB.getEnclosingElement();
		if (!(packagePB instanceof PackageElement) || packagePB.getKind() != ElementKind.PACKAGE) {
			reportError("element AB is not enclosed by a package");
			return false;
		}
		if (!("targets.model.pb".equals(((PackageElement)packagePB).getQualifiedName().toString()))) {
			reportError("The name of package pb is not targets.model.pb");
			return false;
		}
		return true;
	}
	
	/**
	 * Read the annotations on element D (class and method)
	 * @return true if all tests passed
	 */
	private boolean examineDAnnotations() {
		// Examine annotation on class declaration
		List<? extends AnnotationMirror> annotsD = _elementD.getAnnotationMirrors();
		if (null == annotsD || annotsD.isEmpty()) {
			reportError("element D reports no annotations");
			return false;
		}
		for (AnnotationMirror annotD : annotsD) {
			DeclaredType annotDType = annotD.getAnnotationType();
			if (null == annotDType) {
				reportError("annotation mirror of AnnoZ on element D reports null type");
				return false;
			}
			Element annotDElem = annotDType.asElement();
			if (!(annotDElem instanceof TypeElement) || 
					!"targets.model.pa.AnnoZ".equals(((TypeElement)annotDElem).getQualifiedName().toString())) {
				reportError("annotation on element D is not TypeElement targets.model.pa.AnnoZ");
				return false;
			}
			Map<? extends ExecutableElement, ? extends AnnotationValue> values = annotD.getElementValues();
			if (null == values || values.isEmpty()) {
				reportError("@AnnoZ on element D reports no values");
				return false;
			}
			boolean foundStringMethod = false;
			for (Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : values.entrySet()) {
				String methodName = entry.getKey().getSimpleName().toString();
				if ("annoZString".equals(methodName)) {
					foundStringMethod = true;
					Object value = entry.getValue().getValue();
					if (!"annoZOnD".equals(value)) {
						reportError("Value of annoZString param on element D is not \"annoZOnD\"");
						return false;
					}
				}
			}
			if (!foundStringMethod) {
				reportError("Failed to find method annoZString on @AnnoZ on element D");
				return false;
			}
			
			// Check Elements.getElementValuesWithDefaults()
			Map<? extends ExecutableElement, ? extends AnnotationValue> defaults = 
				_elementUtils.getElementValuesWithDefaults(annotD);
			if (null == defaults) {
				reportError("Element.getElementValuesWithDefaults(annotD) returned null");
				return false;
			}
			for (Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : defaults.entrySet()) {
				String methodName = entry.getKey().getSimpleName().toString();
				if ("annoZString".equals(methodName)) {
					foundStringMethod = true;
					Object value = entry.getValue().getValue();
					if (!"annoZOnD".equals(value)) {
						reportError("Explicit value of AnnoZ.annoZString is not \"annoZOnD\"");
						return false;
					}
				}
				else if ("annoZint".equals(methodName)) {
					foundStringMethod = true;
					Object value = entry.getValue().getValue();
					if (null == value || !value.equals(17)) {
						reportError("Default value of AnnoZ.annoZint() is not 17");
						return false;
					}
				}
			}
		}
		
		List<? extends AnnotationMirror> annotsMethodDvoid = _methodDvoid.getAnnotationMirrors();
		if (null == annotsMethodDvoid || annotsMethodDvoid.isEmpty()) {
			reportError("method D.methodDvoid() reports no annotations");
			return false;
		}
		for (AnnotationMirror annotMethodDvoid : annotsMethodDvoid) {
			DeclaredType annotDType = annotMethodDvoid.getAnnotationType();
			if (null == annotDType) {
				reportError("annotation mirror of AnnoZ on D.methodDvoid() reports null type");
				return false;
			}
			Element annotDElem = annotDType.asElement();
			if (!(annotDElem instanceof TypeElement) || 
					!"targets.model.pa.AnnoZ".equals(((TypeElement)annotDElem).getQualifiedName().toString())) {
				reportError("annotation on D.methodDvoid() is not TypeElement targets.model.pa.AnnoZ");
				return false;
			}
			Map<? extends ExecutableElement, ? extends AnnotationValue> values = annotMethodDvoid.getElementValues();
			if (null == values || values.isEmpty()) {
				reportError("@AnnoZ on D.methodDvoid() reports no values");
				return false;
			}
			boolean foundIntMethod = false;
			int order = 0;
			for (Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : values.entrySet()) {
				String methodName = entry.getKey().getSimpleName().toString();
				++order;
				if ("annoZint".equals(methodName)) {
					foundIntMethod = true;
					Object value = entry.getValue().getValue();
					if (!(value instanceof Integer) || (Integer)value != 31) {
						reportError("Value of annoZint param on D.methodDvoid() is not 31");
						return false;
					}
					// Annotation value map should preserve order of annotation values
					if (order != 1) {
						reportError("The annoZint param on D.methodDvoid should be first in the map, but was " + order);
						return false;
					}
				}
			}
			if (!foundIntMethod) {
				reportError("Failed to find method annoZint on @AnnoZ on D.methodDvoid()");
				return false;
			}
		}
		
		// methodDvoid2 is like methodDvoid but the annotation values are in opposite order;
		// check to see that order has been preserved
		List<? extends AnnotationMirror> annotsMethodDvoid2 = _methodDvoid2.getAnnotationMirrors();
		for (AnnotationMirror annotMethodDvoid2 : annotsMethodDvoid2) {
			Map<? extends ExecutableElement, ? extends AnnotationValue> values = annotMethodDvoid2.getElementValues();
			if (null == values || values.size() != 2) {
				reportError("@AnnoZ on D.methodDvoid2() should have two values but had: " +
						(values == null ? 0 : values.size()));
				return false;
			}
			int order = 0;
			for (Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : values.entrySet()) {
				String methodName = entry.getKey().getSimpleName().toString();
				switch (++order) {
				case 1:
					if (!"annoZString".equals(methodName)) {
						reportError("First value of @AnnoZ on D.methodDvoid2 should be annoZString, but was: " + methodName);
						return false;
					}
					break;
				case 2:
					if (!"annoZint".equals(methodName)) {
						reportError("Second value of @AnnoZ on D.methodDvoid2 should be annoZint, but was: " + methodName);
						return false;
					}
					break;
				}
			}
		}
		
		// methodDvoid3 has an annotation with string array type, but its value is a single string.
		// See bug 261969.
		List<? extends AnnotationMirror> annotsMethodDvoid3 = _methodDvoid3.getAnnotationMirrors();
		if (1 != annotsMethodDvoid3.size()) {
			reportError("Wrong number of annotations on D.methodDvoid3(): expected 1, got " + annotsMethodDvoid3.size());
			return false;
		}
		AnnotationMirror annotMethodDvoid3 = annotsMethodDvoid3.get(0);
		Map<? extends ExecutableElement, ? extends AnnotationValue> annotMethodDvoid3Values = annotMethodDvoid3.getElementValues();
		if (1 != annotMethodDvoid3Values.size()) {
			reportError("Wrong number of values on annotation on D.methodDvoid3(): expected 1, got " 
					+ annotMethodDvoid3Values.size());
			return false;
		}
		AnnotationValue annotMethodDvoid3Value = annotMethodDvoid3Values.values().iterator().next();
		Object annotMethodDvoid3RealValue = annotMethodDvoid3Value.getValue();
		if (null == annotMethodDvoid3RealValue) {
			reportError("Value of annotation on D.methodDvoid3() was null");
			return false;
		}
		if (!(annotMethodDvoid3RealValue instanceof List<?>)) {
			reportError("Expected type of annotation on D.methodDvoid3() to be List<?> but was: " +
					annotMethodDvoid3RealValue.getClass().getName());
			return false;
		}
		// If it's a List, then it's a List<AnnotationValue> so we've got another layer to decipher
		AnnotationValue innerDvoid3Value = ((AnnotationValue)((List<?>)annotMethodDvoid3RealValue).get(0));
		if (!"methodDvoid3Value".equals((String)innerDvoid3Value.getValue())) {
			reportError("Expected value of annotation on D.methodDvoid3() to be \"methodDvoid3Value\" but was: " +
					innerDvoid3Value.getValue());
			return false;
		}
		
		return true;
	}
	
	/**
	 * Test the Element.getAnnotation() implementation
	 * @return true if all tests passed
	 */
	private boolean examineGetAnnotation() {
		TypeElement annotatedElement = _elementUtils.getTypeElement("targets.model.pc.AnnotatedWithManyTypes.Annotated");
		if (null == annotatedElement || annotatedElement.getKind() != ElementKind.CLASS) {
			reportError("examineGetAnnotation: couldn't get AnnotatedWithManyTypes.Annotated element");
			return false;
		}
		final String badValue = "examineGetAnnotation: unexpected value for ";
		TypedAnnos.AnnoByte annoByte = annotatedElement.getAnnotation(TypedAnnos.AnnoByte.class);
		if (null == annoByte || annoByte.value() != 3) {
			reportError(badValue + "AnnoByte");
			return false;
		}
		TypedAnnos.AnnoBoolean annoBoolean = annotatedElement.getAnnotation(TypedAnnos.AnnoBoolean.class);
		if (null == annoBoolean || !annoBoolean.value()) {
			reportError(badValue + "AnnoBoolean");
			return false;
		}
		TypedAnnos.AnnoChar annoChar = annotatedElement.getAnnotation(TypedAnnos.AnnoChar.class);
		if (null == annoChar || annoChar.value() != 'c') {
			reportError(badValue + "AnnoChar");
			return false;
		}
		TypedAnnos.AnnoDouble annoDouble = annotatedElement.getAnnotation(TypedAnnos.AnnoDouble.class);
		if (null == annoDouble || annoDouble.value() != 6.3) {
			reportError(badValue + "AnnoDouble");
			return false;
		}
		TypedAnnos.AnnoFloat annoFloat = annotatedElement.getAnnotation(TypedAnnos.AnnoFloat.class);
		if (null == annoFloat || annoFloat.value() != 26.7F) {
			reportError(badValue + "AnnoFloat");
			return false;
		}
		TypedAnnos.AnnoInt annoInt = annotatedElement.getAnnotation(TypedAnnos.AnnoInt.class);
		if (null == annoInt || annoInt.value() != 19) {
			reportError(badValue + "AnnoInt");
			return false;
		}
		TypedAnnos.AnnoLong annoLong = annotatedElement.getAnnotation(TypedAnnos.AnnoLong.class);
		if (null == annoLong || annoLong.value() != 300L) {
			reportError(badValue + "AnnoLong");
			return false;
		}
		TypedAnnos.AnnoShort annoShort = annotatedElement.getAnnotation(TypedAnnos.AnnoShort.class);
		if (null == annoShort || annoShort.value() != 289) {
			reportError(badValue + "AnnoShort");
			return false;
		}
		TypedAnnos.AnnoString annoString = annotatedElement.getAnnotation(TypedAnnos.AnnoString.class);
		if (null == annoString || !"foo".equals(annoString.value())) {
			reportError(badValue + "AnnoString");
			return false;
		}
		TypedAnnos.AnnoEnumConst annoEnumConst = annotatedElement.getAnnotation(TypedAnnos.AnnoEnumConst.class);
		if (null == annoEnumConst || annoEnumConst.value() != TypedAnnos.Enum.A) {
			reportError(badValue + "AnnoEnumConst");
			return false;
		}
		TypedAnnos.AnnoType annoType = annotatedElement.getAnnotation(TypedAnnos.AnnoType.class);
		if (null == annoType) {
			reportError(badValue + "AnnoType");
			return false;
		}
		try {
			Class<?> clazz = annoType.value();
			reportError("examineGetAnnotation: annoType.value() should have thrown a MirroredTypeException but instead returned " + clazz);
			return false;
		}
		catch (MirroredTypeException mte) {
			TypeMirror clazzMirror = mte.getTypeMirror();
			if (null == clazzMirror || clazzMirror.getKind() != TypeKind.DECLARED) {
				reportError("examineGetAnnotation: annoType.value() returned an incorrect mirror: " + clazzMirror);
				return false;
			}
		}
		TypedAnnos.AnnoAnnoChar annoAnnoChar = annotatedElement.getAnnotation(TypedAnnos.AnnoAnnoChar.class);
		if (null == annoAnnoChar || null == annoAnnoChar.value() || 'x' != annoAnnoChar.value().value()) {
			reportError(badValue + "AnnoAnnoChar");
			return false;
		}
		
		TypedAnnos.AnnoArrayInt annoArrayInt = annotatedElement.getAnnotation(TypedAnnos.AnnoArrayInt.class);
		if (null == annoArrayInt) {
			reportError(badValue + "AnnoArrayInt");
			return false;
		}
		int[] arrayInt = annoArrayInt.value();
		if (arrayInt == null || arrayInt.length != 3 || arrayInt[1] != 8) {
			reportError(badValue + "AnnoArrayInt contents");
			return false;
		}
		
		TypedAnnos.AnnoArrayString annoArrayString = annotatedElement.getAnnotation(TypedAnnos.AnnoArrayString.class);
		if (null == annoArrayString) {
			reportError(badValue + "AnnoArrayString");
			return false;
		}
		String[] arrayString = annoArrayString.value();
		if (arrayString == null || arrayString.length != 2 || !"quux".equals(arrayString[1])) {
			reportError(badValue + "AnnoArrayString contents");
			return false;
		}
		//TODO: AnnoArrayAnnoChar
		//TODO: AnnoArrayEnumConst
		TypedAnnos.AnnoArrayType annoArrayType = annotatedElement.getAnnotation(TypedAnnos.AnnoArrayType.class);
		if (null == annoArrayType) {
			reportError(badValue + "AnnoArrayType");
			return false;
		}
		try {
			Class<?>[] contents = annoArrayType.value();
			reportError("examineGetAnnotation: annoArrayType.value() should have thrown a MirroredTypesException but instead returned " + contents);
			return false;
		}
		catch (MirroredTypeException mte) {
			// ignore, because javac incorrectly throws this; see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6519115
		}
		catch (MirroredTypesException mte) {
			List<? extends TypeMirror> clazzMirrors = mte.getTypeMirrors();
			if (null == clazzMirrors || clazzMirrors.size() != 2) {
				reportError("examineGetAnnotation: annoArrayType.value() returned an incorrect mirror list");
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Regression test for bug 261969, retrieving annotation value for string[]-typed
	 * annotation where the actual value is a non-arrayed string.
	 * @return true if all tests passed
	 */
	private boolean bug261969() {
		TypeElement annotatedElement = _elementUtils.getTypeElement("targets.model.pc.Bug261969.Annotated");
		if (null == annotatedElement || annotatedElement.getKind() != ElementKind.CLASS) {
			reportError("bug261969: couldn't get Bug261969.Annotated element");
			return false;
		}
		final String badValue = "bug261969: unexpected value for ";
		TypedAnnos.AnnoArrayString annoArrayString = annotatedElement.getAnnotation(TypedAnnos.AnnoArrayString.class);
		if (null == annoArrayString) {
			reportError(badValue + "AnnoArrayString");
			return false;
		}
		String[] arrayString = annoArrayString.value();
		if (arrayString == null || arrayString.length != 1 || !"foo".equals(arrayString[0])) {
			reportError(badValue + "AnnoArrayString contents");
			return false;
		}
		return true;
	}
}
