/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *     Terry Parker <tparker@google.com> - Enable the Java model caches to recover from IO errors - https://bugs.eclipse.org/455042
 *******************************************************************************/
package org.eclipse.jdt.core.tests.model;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;

import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.*;
import org.eclipse.jdt.core.tests.util.Util;
import org.eclipse.jdt.internal.core.ClasspathEntry;
import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.core.NameLookup;

import junit.framework.Test;

/**
 * These test ensure that modifications in Java projects are correctly reported as
 * IJavaEllementDeltas.
 */
public class NameLookupTests2 extends ModifyingResourceTests {

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

	// Use this static initializer to specify subset for tests
	// All specified tests which do not belong to the class are skipped...
	static {
//		org.eclipse.jdt.internal.core.search.matching.MatchLocator.PRINT_BUFFER = false;
//		TESTS_PREFIX =  "testArray";
//		TESTS_NAMES = new String[] { "testFindBinaryTypeWithDollarName" };
//		TESTS_NUMBERS = new int[] { 8 };
//		TESTS_RANGE = new int[] { 6, -1 };
	}

public static Test suite() {
	return buildModelTestSuite(NameLookupTests2.class);
}

NameLookup getNameLookup(JavaProject project) throws JavaModelException {
	return project.newNameLookup((WorkingCopyOwner)null);
}
public void testAddPackageFragmentRootAndPackageFrament() throws CoreException {
	try {
		IJavaProject p1 = createJavaProject("P1", new String[] {"src1"}, "bin");
		IJavaProject p2 = createJavaProject("P2", new String[] {}, "");
		IClasspathEntry[] classpath =
			new IClasspathEntry[] {
				JavaCore.newProjectEntry(new Path("/P1"))
			};
		p2.setRawClasspath(classpath, null);

		IPackageFragment[] res = getNameLookup((JavaProject)p2).findPackageFragments("p1", false);
		assertTrue("Should get no package fragment", res == null);

		IClasspathEntry[] classpath2 =
			new IClasspathEntry[] {
				JavaCore.newSourceEntry(new Path("/P1/src1")),
				JavaCore.newSourceEntry(new Path("/P1/src2"))
			};
		p1.setRawClasspath(classpath2, null);
		createFolder("/P1/src2/p1");

		res = getNameLookup((JavaProject)p2).findPackageFragments("p1", false);
		assertTrue(
			"Should get 'p1' package fragment",
			res != null &&
			res.length == 1 &&
			res[0].getElementName().equals("p1"));

	} finally {
		deleteProject("P1");
		deleteProject("P2");
	}
}
public void testAddPackageFragment() throws CoreException {
	try {
		createJavaProject("P1", new String[] {"src1"}, "bin");
		IJavaProject p2 = createJavaProject("P2", new String[] {}, "");
		IClasspathEntry[] classpath =
			new IClasspathEntry[] {
				JavaCore.newProjectEntry(new Path("/P1"))
			};
		p2.setRawClasspath(classpath, null);

		IPackageFragment[] res = getNameLookup((JavaProject)p2).findPackageFragments("p1", false);
		assertTrue("Should get no package fragment", res == null);

		createFolder("/P1/src1/p1");

		res = getNameLookup((JavaProject)p2).findPackageFragments("p1", false);
		assertTrue(
			"Should get 'p1' package fragment",
			res != null &&
			res.length == 1 &&
			res[0].getElementName().equals("p1"));

	} finally {
		deleteProject("P1");
		deleteProject("P2");
	}
}
/*
 * Resolve, add pkg, resolve again: new pkg should be accessible
 * (regression test for bug 37962 Unexpected transient problem during reconcile
 */
public void testAddPackageFragment2() throws CoreException {
	try {
		JavaProject project = (JavaProject)createJavaProject("P", new String[] {"src"}, "bin");
		createFolder("/P/src/p1");

		IPackageFragment[] pkgs = getNameLookup(project).findPackageFragments("p1", false);
		assertElementsEqual(
			"Didn't find p1",
			"p1 [in src [in P]]",
			pkgs);

		createFolder("/P/src/p2");

		pkgs = getNameLookup(project).findPackageFragments("p2", false);
		assertElementsEqual(
			"Didn't find p2",
			"p2 [in src [in P]]",
			pkgs);
	} finally {
		deleteProject("P");
	}
}
/*
 * Ensures that a NameLookup can be created with working copies that contain duplicate types
 * (regression test for bug 63245 findPackageFragment won't return default package)
 */
public void testDuplicateTypesInWorkingCopies() throws CoreException {
//	ICompilationUnit[] workingCopies = new ICompilationUnit[3];
	this.workingCopies = new ICompilationUnit[3];
	try {
		JavaProject project = (JavaProject)createJavaProject("P");
		this.workingCopies[0] = getWorkingCopy(
			"/P/X.java",
			"public class X {\n" +
			"}\n" +
			"class Other {\n" +
			"}"
		);
		this.workingCopies[1] = getWorkingCopy(
			"/P/Y.java",
			"public class Y {\n" +
			"}\n" +
			"class Other {\n" +
			"}"
		);
		this.workingCopies[2] = getWorkingCopy(
			"/P/Z.java",
			"public class Z {\n" +
			"}\n" +
			"class Other {\n" +
			"}"
		);
		NameLookup nameLookup = project.newNameLookup(this.workingCopies);
		IType type = nameLookup.findType("Other", false, NameLookup.ACCEPT_ALL); // TODO (jerome) should use seekTypes
		assertTypesEqual(
			"Unepexted types",
			"Other\n",
			new IType[] {type}
		);
	} finally {
//		discardWorkingCopies(workingCopies);
		deleteProject("P");
	}
}
/*
 * Find a default package fragment in a non-default root by its path.
 * (regression test for bug 63245 findPackageFragment won't return default package)
 */
public void testFindDefaultPackageFragmentInNonDefaultRoot() throws CoreException {
	try {
		JavaProject project = (JavaProject)createJavaProject("P", new String[] {"src"}, "bin");

		IPackageFragment pkg = getNameLookup(project).findPackageFragment(new Path("/P/src"));
		assertElementsEqual(
			"Didn't find default package",
			"<default> [in src [in P]]",
			new IJavaElement[] {pkg});

	} finally {
		deleteProject("P");
	}
}
/*
 * Creates a package fragment and finds it in a dependent project in a batch operation
 * (regression test for bug 144776 JavaProject.resetCaches() needs to reset dependent projects
 */
public void testNameLookupFindPackageFragmentAfterCreation() throws CoreException {
	try {
		final IJavaProject p1 = createJavaProject("P1");
		final JavaProject p2 = (JavaProject) createJavaProject("P2", new String[] {""}, new String[0], new String[] {"/P1"}, "");

		// populate namelookup for p2
		getNameLookup(p2);

		IWorkspaceRunnable runnable = new IWorkspaceRunnable(){
			public void run(IProgressMonitor monitor) throws CoreException {
				p1.getPackageFragmentRoot(p1.getProject()).createPackageFragment("pkg", false/*don't force*/, monitor);
				IPackageFragment[] pkgs = getNameLookup(p2).findPackageFragments("pkg", false/*exact match*/);
				assertElementsEqual(
					"Unexpected package fragments",
					"pkg [in <project root> [in P1]]",
					pkgs);
			}
		};
		JavaCore.run(runnable, null/*no progress*/);
	} finally {
		deleteProject("P1");
		deleteProject("P2");
	}
}
/*
 * Ensure that finding a package fragment with a working copy opened returns one element only
 * (regression test for bug 89624 Open on selection proposes twice the same entry)
 */
public void testFindPackageFragmentWithWorkingCopy() throws CoreException {
	this.workingCopies = new ICompilationUnit[1];
	try {
		JavaProject project = (JavaProject)createJavaProject("P");
		createFolder("/P/p1");
		this.workingCopies[0] = getWorkingCopy(
			"/P/p1/X.java",
			"package p1;\n" +
			"public class X {\n" +
			"}"
		);
		NameLookup nameLookup = project.newNameLookup(this.workingCopies);
		IJavaElement[] pkgs = nameLookup.findPackageFragments("p1", false/*not a partial match*/);
		assertElementsEqual(
			"Unexpected packages",
			"p1 [in <project root> [in P]]",
			pkgs);
	} finally {
		deleteProject("P");
	}
}
/*
 * Ensure that a package fragment with a path with a length equals to an external jar path length + 1
 * is not found
 * (regression test for bug 266771 NameLookup.findPackageFragment returns very incorrect package fragments)
 */
public void testFindPackageFragment2() throws CoreException {
	try {
		JavaProject project = (JavaProject)createJavaProject("P", new String[0], new String[] {"JCL_LIB"}, "bin");
		NameLookup nameLookup =getNameLookup(project);
		IPath pathToSearch = new Path(getExternalJCLPathString() + 'a');
		IPackageFragment pkg = nameLookup.findPackageFragment(pathToSearch);
		assertElementEquals(
			"Unexpected package",
			"<null>",
			pkg);
	} finally {
		deleteProject("P");
	}
}

/*
 * Ensure that a member type with a name ending with a dollar and a number is found
 * (regression test for bug 103466 Stack Overflow: Requesting Java AST from selection)
 */
public void testFindBinaryTypeWithDollarName() throws CoreException, IOException {
	try {
		IJavaProject project = createJavaProject("P");
		addLibrary(project, "lib.jar", "libsrc.zip",
			new String[] {
				"p/X.java",
				"package p;\n" +
				"public class X {\n" +
				"  public class $1 {\n" +
				"    public class $2 {\n" +
				"    }\n" +
				"  }\n" +
				"}"
			},
			"1.4");
		IType type = getNameLookup((JavaProject) project).findType("p.X$$1", false, NameLookup.ACCEPT_ALL);
		assertTypesEqual(
			"Unexpected type",
			"p.X$$1\n",
			new IType[] {type});
	} finally {
		deleteProject("P");
	}
}
/*
 * Ensure that a type with the same simple name as its member type is found
 * (regression test for bug 102286 Error when trying F4-Type Hierarchy)
 */
public void testFindBinaryTypeWithSameNameAsMember() throws CoreException, IOException {
	try {
		IJavaProject project = createJavaProject("P", new String[] {}, new String[] {"/P/lib"}, new String[] {}, "bin");
		createFolder("/P/lib/p");
		createFile("/P/lib/p/X.class", "");
		createFile("/P/lib/p/X$X.class", "");
		IType type = getNameLookup((JavaProject) project).findType("p.X", false, NameLookup.ACCEPT_ALL);
		assertTypesEqual(
			"Unexpected type",
			"p.X\n",
			new IType[] {type});
	} finally {
		deleteProject("P");
	}
}
/*
 * Ensures that a type in working copy opened in an unrelated project is not found
 * (regression test for bug 169970 [model] code assist favorites must honour build path of project in context)
 */
public void testFindTypeWithUnrelatedWorkingCopy() throws Exception {
	ICompilationUnit workingCopy = null;
	try {
		createJavaProject("P1");
		createFolder("/P1/p");
		createFile(
			"/P1/p/X.java",
			"package p;\n" +
			"public class X {\n" +
			"}"
		);
		workingCopy = getCompilationUnit("/P1/p/X.java");
		workingCopy.becomeWorkingCopy(null);
		IJavaProject p2 = createJavaProject("P2");
		NameLookup nameLookup = ((JavaProject) p2).newNameLookup(DefaultWorkingCopyOwner.PRIMARY);
		IType type = nameLookup.findType("p.X", false, NameLookup.ACCEPT_ALL);
		assertNull("Should not find p.X in P2", type);
	} finally {
		if (workingCopy != null)
			workingCopy.discardWorkingCopy();
		deleteProject("P1");
		deleteProject("P2");
	}
}
/*
 * A test for bug 162621. Tests that a library jar that is initially invalid but transitions
 * to being valid becomes visible in name lookup. Previously the jar would stay in the invalid
 * archive cache until a classpath change was generated, and would not make it into the project's
 * JavaProjectElementInfo cache without restarting Eclipse or closing and reopening the project.
 */
public void testTransitionFromInvalidToValidJar() throws CoreException, IOException {
	/*
	 * Since it is difficult to test intermittent IO errors, simulate it
	 * by creating two jars of equal size, one of which has an invalid format.
	 * Set up the classpath with the invalid jar, and then swap in the valid jar
	 * and reset its timestamp to be the same as the original file.
	 */
	String goodJar = getExternalPath() + "goodJar.jar";
	String transitioningJar = getExternalPath() + "transitioningJar.jar";
	java.nio.file.Path goodJarPath = FileSystems.getDefault().getPath(goodJar);
	java.nio.file.Path transitioningJarPath = FileSystems.getDefault().getPath(transitioningJar);
	IPath transitioningIPath = Path.fromOSString(transitioningJar);

	try {
		Util.createJar(
				new String[] {
						"test1/IResource.java", //$NON-NLS-1$
						"package test1;\n" + //$NON-NLS-1$
						"public class IResource {\n" + //$NON-NLS-1$
						"}" //$NON-NLS-1$
					},
				new String[] {
					"META-INF/MANIFEST.MF",
					"Manifest-Version: 1.0\n"
				},
				goodJar,
				JavaCore.VERSION_1_4);
		char[] invalidContents = new char[(int) goodJarPath.toFile().length()];
		Arrays.fill(invalidContents, ' ');
		Util.createFile(transitioningJar, String.copyValueOf(invalidContents));

		// Set up the project with the invalid jar and allow all of the classpath validation
		// and delta processing to complete.
		JavaProject proj = (JavaProject) createJavaProject("P", new String[] {}, new String[] {transitioningJar}, "bin");
		JavaModelManager.getJavaModelManager().getJavaModel().refreshExternalArchives(null, null);
		waitForAutoBuild();

		assertTrue("The invalid archive cache should report that the jar is invalid",
				JavaModelManager.getJavaModelManager().isInvalidArchive(transitioningIPath));
		IType type = getNameLookup(proj).findType("test1.IResource", false, NameLookup.ACCEPT_CLASSES);
		assertEquals("Name lookup should fail when the jar is invalid", null, type);

		// Substitute the good jar, maintaining the timestamp.
		FileTime fileTime = Files.getLastModifiedTime(transitioningJarPath);
		Files.move(goodJarPath, transitioningJarPath, StandardCopyOption.REPLACE_EXISTING);
		Files.setLastModifiedTime(transitioningJarPath, fileTime);

		// Since the timestamp hasn't changed, an external archive refresh isn't going
		// to update the caches or cause name lookups to work.
		JavaModelManager.getJavaModelManager().getJavaModel().refreshExternalArchives(null, null);
		assertTrue("External archive refresh sees no changes, so the invalid archive cache should be unchanged",
				JavaModelManager.getJavaModelManager().isInvalidArchive(transitioningIPath));
		type = getNameLookup(proj).findType("test1.IResource", false, NameLookup.ACCEPT_CLASSES);
		assertEquals("External archive refresh sees no changes, so the project cache should be unchanged",
				null, type);

		// Re-validating the jar via validateClasspathEntry() forces eviction from the
		// invalid archive cache more quickly than waiting for the time-based eviction,
		// allowing the test to run more quickly.
		IClasspathEntry transitioningEntry = JavaCore.newLibraryEntry(transitioningIPath, null, null);
		ClasspathEntry.validateClasspathEntry(proj, transitioningEntry, false, false);

		assertFalse("The invalid archive cache should no longer report the jar as invalid",
				JavaModelManager.getJavaModelManager().isInvalidArchive(transitioningIPath));
		type = getNameLookup(proj).findType("test1.IResource", false, NameLookup.ACCEPT_CLASSES);
		assertFalse("Name lookup should be able to find types in the valid jar", type == null);
	} finally {
		Files.deleteIfExists(goodJarPath);
		Files.deleteIfExists(transitioningJarPath);
		deleteProject("P");
	}
}
}

