/*******************************************************************************
 * Copyright (c) 2008 - 2017 Walter Harley and others
 *
 * 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:
 *    eclipse@cafewalter.com 			- initial API and implementation
 *    Harry Terkelsen <het@google.com> 	- Contribution for Bug 437414 - Annotation processing is broken when build is batched
 *    Fabian Steeg <steeg@hbz-nrw.de> - Pass automatically provided options to Java 6 processors - https://bugs.eclipse.org/341298
 *******************************************************************************/

package org.eclipse.jdt.apt.pluggable.tests;

import java.util.List;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.apt.core.util.AptConfig;
import org.eclipse.jdt.apt.pluggable.tests.processors.buildertester.Bug341298Processor;
import org.eclipse.jdt.apt.pluggable.tests.processors.buildertester.Bug468893Processor;
import org.eclipse.jdt.apt.pluggable.tests.processors.buildertester.Bug510118Processor;
import org.eclipse.jdt.apt.pluggable.tests.processors.buildertester.BugsProc;
import org.eclipse.jdt.apt.pluggable.tests.processors.buildertester.InheritedAnnoProc;
import org.eclipse.jdt.apt.pluggable.tests.processors.buildertester.TestFinalRoundProc;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.tests.builder.Problem;
import org.eclipse.jdt.internal.core.builder.AbstractImageBuilder;

import junit.framework.Test;
import junit.framework.TestSuite;

/**
 * Tests covering the IDE's ability to process the correct set of files.
 */
public class BuilderTests extends TestBase
{

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

	public static Test suite() {
		return new TestSuite(BuilderTests.class);
	}
	
	/**
	 * Verify that a new type generated in the final round does not get
	 * annotations processed, but does get compiled. The JSR269 spec is somewhat
	 * vague about whether it should be possible to generate a new type during
	 * the final round (since the final round does not happen until after a
	 * round in which no new types are generated); but apparently javac behaves
	 * this way.
	 * <p>
	 * See <a href="http://bugs.eclipse.org/329156">Bug 329156</a> and <a
	 * href="http://bugs.sun.com/view_bug.do?bug_id=6634138">the corresponding
	 * bug in javac</a>, which Sun fixed.
	 */
	public void testFinalRound() throws Throwable {
		ProcessorTestStatus.reset();
		TestFinalRoundProc.resetNumRounds();
		IJavaProject jproj = createJavaProject(_projectName);
		disableJava5Factories(jproj);
		IProject proj = jproj.getProject();
		IPath projPath = proj.getFullPath();
		IPath root = projPath.append("src");
		
		// The @FinalRoundTestTrigger processor does not generate any files when it
		// first runs; but on its final round it then generates a new Java type
		// that is annotated with @FinalRoundTestTrigger.
		env.addClass(root, "t", "Foo",
				"package t;\n" +
				"import org.eclipse.jdt.apt.pluggable.tests.annotations.FinalRoundTestTrigger;\n" +
				"@FinalRoundTestTrigger\n" +
				"public class Foo {}"
		);
		AptConfig.setEnabled(jproj, true);
		
		fullBuild();
		expectingNoProblems();
		
		// Processor should have run total of two rounds; compiled classes
		// should include Foo and FinalRoundGen.
		assertEquals(2, TestFinalRoundProc.getNumRounds());
		expectingUniqueCompiledClasses(new String[] {"t.Foo", "g.FinalRoundGen"});
	}
	
	/**
	 * Verify that a class whose superclass is annotated with an inherited annotation
	 * gets treated the same as if the annotation were present on the class itself.
	 * See <a href="http://bugs.eclipse.org/270754">Bug 270754</a>.
	 */
	public void disabled_testInheritedAnnotation() throws Throwable {
		ProcessorTestStatus.reset();
		IJavaProject jproj = createJavaProject(_projectName);
		disableJava5Factories(jproj);
		IProject proj = jproj.getProject();
		IPath projPath = proj.getFullPath();
		IPath root = projPath.append("src");
		
		env.addClass(root, "", "Base",
				"import org.eclipse.jdt.apt.pluggable.tests.annotations.InheritedTrigger;\n" +
				"@InheritedTrigger(0)\n" +
				"public class Base {}"
		);

		// Because Sub extends Base, it should be treated as if it were annotated with InheritedTrigger
		env.addClass(root, "", "Sub",
				"public class Sub extends Base {\n" +
				"}"
		);
			
		AptConfig.setEnabled(jproj, true);
		
		fullBuild();
		expectingNoProblems();
		List<String> elements = InheritedAnnoProc.getProcessedElements();
		assertTrue(elements.contains("Base"));
		assertTrue(elements.contains("Sub"));
		assertTrue("Processor did not run", ProcessorTestStatus.processorRan());
		assertEquals("Processor reported errors", ProcessorTestStatus.NO_ERRORS, ProcessorTestStatus.getErrors());

		// Modify base class and verify that both base and subclass get processed
		InheritedAnnoProc.clearProcessedElements();
		env.addClass(root, "", "Base",
				"import org.eclipse.jdt.apt.pluggable.tests.annotations.InheritedTrigger;\n" +
				"@InheritedTrigger(1)\n" +
				"public class Base {}"
		);
		incrementalBuild();
		expectingNoProblems();
		elements = InheritedAnnoProc.getProcessedElements();
		assertTrue(elements.contains("Base"));
		assertTrue(elements.contains("Sub"));
		assertTrue("Processor did not run", ProcessorTestStatus.processorRan());
		assertEquals("Processor reported errors", ProcessorTestStatus.NO_ERRORS, ProcessorTestStatus.getErrors());
		
		// Modify subclass and verify that it gets processed
		InheritedAnnoProc.clearProcessedElements();
		env.addClass(root, "", "Sub",
				"public class Sub extends Base {\n" +
				" // this line is new\n" +
				"}"
		);
		incrementalBuild();
		expectingNoProblems();
		elements = InheritedAnnoProc.getProcessedElements();
		assertTrue(elements.contains("Sub"));
		assertTrue("Processor did not run", ProcessorTestStatus.processorRan());
		assertEquals("Processor reported errors", ProcessorTestStatus.NO_ERRORS, ProcessorTestStatus.getErrors());
	}
	
	/**
	 * Deleting FooEvent caused the reference to FooEvent in 
	 * FooImpl to appear as a package in one case and as a type in another,
	 * in turn causing a null binding to be passed to APT.
	 *
	 * See <a href="http://bugs.eclipse.org/295948">Bug 295948</a>.
	 */
	public void testBug295948() throws Throwable {
		ProcessorTestStatus.reset();
		IJavaProject jproj = createJavaProject(_projectName);
		disableJava5Factories(jproj);
		IProject proj = jproj.getProject();
		IPath projPath = proj.getFullPath();
		IPath root = projPath.append("src");
		
		env.addClass(root, "test295948", "FooEvent",
				"package test295948;\n" +
				"public class FooEvent {\n" + 
				"    public interface Handler {\n" + 
				"        void handle(FooEvent event);\n" + 
				"    }\n" + 
				"}\n" + 
				"\n"
		);
		
		IPath fooImplClass = env.addClass(root, "test295948", "FooImpl",
				"package test295948;\n" +
				"public class FooImpl implements FooEvent.Handler {\n" + 
				"    @Override\n" + 
				"    public void handle(FooEvent event) {\n" + 
				"    }\n" + 
				"}\n"
		);
		
		AptConfig.setEnabled(jproj, true);
		
		fullBuild();
		expectingNoProblems();
		
		// Delete FooEvent and recompile
		proj.findMember("src/test295948/FooEvent.java").delete(false, null);
		incrementalBuild();
		expectingProblemsFor(fooImplClass,
				"Problem : FooEvent cannot be resolved to a type [ resource : </" + _projectName + "/src/test295948/FooImpl.java> range : <108,116> category : <40> severity : <2>]\n" + 
				"Problem : FooEvent cannot be resolved to a type [ resource : </" + _projectName + "/src/test295948/FooImpl.java> range : <52,60> category : <40> severity : <2>]");
	}	
	// https://bugs.eclipse.org/bugs/show_bug.cgi?id=407841
	public void testBug407841() throws Throwable {
		int old = org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.MAX_AT_ONCE;
		try {
			org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.MAX_AT_ONCE =1;
			ProcessorTestStatus.reset();
			IJavaProject jproj = createJavaProject(_projectName);
			disableJava5Factories(jproj);
			IProject proj = jproj.getProject();
			IdeTestUtils.copyResources(proj, "targets/bug407841", "src/targets/bug407841");
			
			AptConfig.setEnabled(jproj, true);
			fullBuild();
			expectingNoProblems();
			assertEquals("Elements should have been processed", 0, BugsProc.getUnprocessedElements());
			assertEquals("Elements should have been processed", 3, BugsProc.getNumRounds());
		} finally {
			org.eclipse.jdt.internal.core.builder.AbstractImageBuilder.MAX_AT_ONCE = old;
		}
	}
	public void testBug468893() throws Throwable {
		ProcessorTestStatus.reset();
		IJavaProject jproj = createJavaProject(_projectName);
		disableJava5Factories(jproj);
		IProject proj = jproj.getProject();
		IdeTestUtils.copyResources(proj, "targets/bug468893", "src/targets/bug468893");

		AptConfig.setEnabled(jproj, true);
		fullBuild();
		expectingNoProblems();
		assertEquals("Should have been processed over just once", 1, Bug468893Processor.count());
	}
	// https://bugs.eclipse.org/bugs/show_bug.cgi?id=419769
	public void testBug419769() throws Throwable {
		try {
			ProcessorTestStatus.reset();
			IJavaProject jproj = createJavaProject(_projectName);
			disableJava5Factories(jproj);
			IProject proj = jproj.getProject();
			IdeTestUtils.copyResources(proj, "targets/bug419769", "src/targets/bug419769");
			AptConfig.setEnabled(jproj, true);
			fullBuild();
			expectingNoProblems(); // There should be no compiler errors, i.e. except the APT injected ones.
			Problem[] problems = env.getProblemsFor(jproj.getProject().getFullPath(), "org.eclipse.jdt.apt.pluggable.core.compileProblem");
			StringBuffer buf = new StringBuffer();
			for (int i = 0, length = problems.length; i < length; i++) {
				Problem problem = problems[i];
				buf.append(problem.getMessage());
				if (i < length - 1) buf.append('\n');
			}
			assertEquals("There should be two reported problems",
						"Some Error Message.\nYet another Error Message.",
						buf.toString());
		} finally {
		}
	}
	
	public void testBug387956() throws Exception {
		ProcessorTestStatus.reset();
		IJavaProject jproj = createJavaProject(_projectName);
		disableJava5Factories(jproj);
		IProject proj = jproj.getProject();
		IdeTestUtils.copyResources(proj, "targets/bug387956", "src/targets/bug387956");

		AptConfig.setEnabled(jproj, true);
		fullBuild();
		expectingNoProblems();
	}
	public void testBatchedBuild() throws Throwable {
		int old = AbstractImageBuilder.MAX_AT_ONCE;
		IJavaProject jproj = createJavaProject(_projectName);
		disableJava5Factories(jproj);
		IProject proj = jproj.getProject();
		IPath projPath = proj.getFullPath();
		IPath root = projPath.append("src");
		IPath packagePath = root.append("test");
		try {
			// Force the build to be batched
			AbstractImageBuilder.MAX_AT_ONCE = 1;
			ProcessorTestStatus.reset();
			
			env.addClass(root, "test", "Foo",
					"package test;\n" +
					"import org.eclipse.jdt.apt.pluggable.tests.annotations.GenClass6;\n" +
			        "@GenClass6(name = \"FooGen\", pkg = \"test\")\n" +
				    "public class Foo {\n" +
			        "    public Bar bar;\n" +
				    "}");
			env.addClass(root, "test", "Bar",
					"package test;\n" +
					"import org.eclipse.jdt.apt.pluggable.tests.annotations.GenClass6;\n" +
			        "@GenClass6(name = \"BarGen\", pkg = \"test\")\n" +
				    "public class Bar {\n" +
			        "    public Foo foo;\n" +
				    "}");
			AptConfig.setEnabled(jproj, true);
			
			fullBuild();
			expectingNoProblems();
			expectingUniqueCompiledClasses(
					new String[] {"test.Foo", "test.Bar", "test.FooGen", "test.BarGen"});
			
		} finally {
			AbstractImageBuilder.MAX_AT_ONCE = old;
			env.removeClass(packagePath, "Foo");
			env.removeClass(packagePath, "Bar");
			env.removeClass(packagePath, "FooGen");
			env.removeClass(packagePath, "BarGen");

		}
	}
	
	public void testBug468853() throws Throwable {
		int old = AbstractImageBuilder.MAX_AT_ONCE;
		IJavaProject jproj = createJavaProject(_projectName);
		disableJava5Factories(jproj);
		IProject proj = jproj.getProject();
		IPath projPath = proj.getFullPath();
		IPath root = projPath.append("src");
		IPath packagePath = root.append("test");
		try {
			// Force the build to be batched
			AbstractImageBuilder.MAX_AT_ONCE = 2;
			ProcessorTestStatus.reset();
			
			env.addClass(root, "test", "Foo",
					"package test;\n" +
							"import org.eclipse.jdt.apt.pluggable.tests.annotations.GenClass6;\n" +
							"import org.eclipse.jdt.apt.pluggable.tests.annotations.Message6;\n" +
							"import javax.tools.Diagnostic.Kind;\n" +
							"@GenClass6(name = \"FooGen\", pkg = \"test\", rounds = 2)\n" +
							"@Message6(text = \"APT message\", value = Kind.ERROR)\n" +
							"public class Foo extends FooGen {\n" +
							"    public Bar bar;\n" +
					"}");
			env.addClass(root, "test", "Bar",
					"package test;\n" +
							"public class Bar {\n" +
							"    public Foo foo;\n" +
					"}");
			AptConfig.setEnabled(jproj, true);
			
			fullBuild();
			expectingNoProblems();
			expectingUniqueCompiledClasses(
					new String[] {"test.Foo", "test.Bar", "test.FooGen", "test.FooGenGen"});
			
		} finally {
			AbstractImageBuilder.MAX_AT_ONCE = old;
			env.removeClass(packagePath, "Foo");
			env.removeClass(packagePath, "Bar");
			env.removeClass(packagePath, "FooGen");
			
		}
	}

	public void testBug510118() throws Throwable {
		ProcessorTestStatus.reset();
		IJavaProject jproj = createJavaProject(_projectName);
		disableJava5Factories(jproj);
		IProject proj = jproj.getProject();
		IdeTestUtils.copyResources(proj, "targets/bug510118", "src/targets/bug510118");

		AptConfig.setEnabled(jproj, true);
		fullBuild();
		expectingNoProblems();
		assertTrue("Incorrect status received from annotation processor", Bug510118Processor.status());
	}

	public void testBug341298() throws Throwable {
		ProcessorTestStatus.reset();
		IJavaProject project = createJavaProject(_projectName);
		IPath root = project.getProject().getFullPath().append("src");
		env.addClass(root, "test341298", "Annotated",
				"package test341298;\n" +
				"@Annotation public class Annotated {}"
		);
		env.addClass(root, "test341298", "Annotation",
				"package test341298;\n" +
				"public @interface Annotation {}"
		);
		AptConfig.addProcessorOption(project, "classpath", "%classpath%");
		AptConfig.addProcessorOption(project, "sourcepath", "%sourcepath%");
		AptConfig.addProcessorOption(project, "phase", "%test341298%");
		AptConfig.setEnabled(project, true);
		fullBuild();
		assertTrue("Processor should be able to compile with passed options", Bug341298Processor.success());
	}
	
	public void testBug539663() throws Throwable {
		if (!canRunJava9()) {
			return;
		}
		ProcessorTestStatus.reset();
		IJavaProject jproj = createJava9Project(_projectName);
		disableJava5Factories(jproj);
		IProject proj = jproj.getProject();
		IPath projPath = proj.getFullPath();
		IPath root = projPath.append("src");
		IPath packagePath = root.append("test");
		try {
			env.addClass(root, null, "module-info", "module example {requires annotations;}");
			
			env.addClass(root, "test", "Foo",
					"package test;\n" +
					"import org.eclipse.jdt.apt.pluggable.tests.annotations.GenClass6;\n" +
			        "@GenClass6(name = \"ImmutableFoo\", pkg = \"test\")\n" +
				    "public class Foo {\n" +
			        "    public void f(ImmutableFoo o) { }\n" +
				    "}");
			AptConfig.setEnabled(jproj, true);
			
			fullBuild();
			expectingNoProblems();			
		} finally {
			env.removeClass(packagePath, "module-info");
			env.removeClass(packagePath, "Foo");
			env.removeClass(packagePath, "ImmutableFoo");
		}
	}
}
