/*******************************************************************************
 * Copyright (c) 2000, 2011 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
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.ui.tests.text;

import junit.framework.TestCase;
import junit.framework.TestSuite;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.IHyperlink;

import org.eclipse.cdt.core.dom.IPDOMManager;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.model.ICContainer;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ILanguage;
import org.eclipse.cdt.core.model.IWorkingCopy;
import org.eclipse.cdt.core.testplugin.CProjectHelper;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.IWorkingCopyManager;
import org.eclipse.cdt.ui.testplugin.EditorTestHelper;

import org.eclipse.cdt.internal.core.model.ASTCache.ASTRunnable;

import org.eclipse.cdt.internal.ui.editor.ASTProvider;
import org.eclipse.cdt.internal.ui.editor.CEditor;
import org.eclipse.cdt.internal.ui.editor.CElementHyperlinkDetector;

/**
 * This test just checks that hyperlinks are created in the right
 * places. It does not test that the hyperlinks actually take you 
 * to the right place.
 * 
 * @author Mike Kucera
 */
public class HyperlinkTest extends TestCase {
	private static final String CPP_FILE_NAME_1 = "hyperlink_test_cpp.cpp";
	private static final String CPP_FILE_NAME_2 = "hyperlink_test_cpp_without_ast.cpp";
	private static final String CPP_CODE = 
		"#include <stdio.h> \n" +
		"#define SOMEMACRO macro_token1 macro_token2 \n" +
		"// COMMENT there should not be links inside of comments \n" +
		"class Point { \n" +
		"  public: \n" +
		"    Point(); \n" +
		"    ~Point(); \n" +
		"    void set(int x, int y); \n" +
		"    int getX(); \n" +
		"    int getY(); \n" +
		"    Point operator+(Point); \n" +
		"  private: \n" +
		"    int x, y; \n" +
		"}; \n" +
		"#define SOMEMACROFUNCTION(a) function(a) \n" +
		"int test(Point p) {  \n" +
		"    char* str = \"STRING LITERAL\"; \n" +
		"    p + p;" +
		"    int arg;" +
		"    SOMEMACROFUNCTION(arg);" +
		"} \n";
	
	private static final String C_FILE_NAME_1 = "hyperlink_test_c_1.c";
	private static final String C_CODE_1 = 
		"int main() { \n" +
		"    int class = 99; \n" +
		"}";
	
	private static final String C_FILE_NAME_2 = "hyperlink_test_c_2.c";
	private static final String C_CODE_2 = 
		"#ifdef NOTDEF\n" +
		"    int nothere = 99; \n" +
		"#else\n" +
		"    int itworks = 100; \n" +
		"#endif\n";

	private ICProject project;
	private CEditor editor;

	public static TestSuite suite() {
		return new TestSuite(HyperlinkTest.class);
	}
	
	private void setUpEditor(String fileName, String code) throws Exception {
		setUpEditor(fileName, code, true);
	}

	private void setUpEditor(String fileName, String code, boolean buildAST) throws Exception {
		super.setUp();
		project= CProjectHelper.createCCProject(super.getName(), "unused", IPDOMManager.ID_NO_INDEXER);
		ICContainer cContainer= CProjectHelper.addCContainer(project, "src");
		IFile file= EditorTestHelper.createFile((IContainer) cContainer.getResource(), fileName, code, new NullProgressMonitor());
		
		assertNotNull(file);
		assertTrue(file.exists());
		editor = (CEditor) EditorTestHelper.openInEditor(file, true);
		EditorTestHelper.joinReconciler(EditorTestHelper.getSourceViewer(editor), 10, 1000, 10);
		if (buildAST) {
			// Since CElementHyperlinkDetector doesn't wait for an AST to be created,
			// we trigger AST creation ahead of time.
			IWorkingCopyManager manager = CUIPlugin.getDefault().getWorkingCopyManager();
			IWorkingCopy workingCopy = manager.getWorkingCopy(editor.getEditorInput());
			IStatus status= ASTProvider.getASTProvider().runOnAST(workingCopy, ASTProvider.WAIT_IF_OPEN, null, new ASTRunnable() {
				@Override
				public IStatus runOnAST(ILanguage lang, IASTTranslationUnit ast) {
					return Status.OK_STATUS;
				}
			});
		}
	}
	
	@Override
	protected void tearDown() throws Exception {
		EditorTestHelper.closeEditor(editor);
		CProjectHelper.delete(project);
	}
	
	private IHyperlink[] getHyperlinks(int mouseOffset) {
		CElementHyperlinkDetector detector = new CElementHyperlinkDetector();
		detector.setContext(editor);
		IRegion region = new Region(mouseOffset, 0);
		return detector.detectHyperlinks(EditorTestHelper.getSourceViewer(editor), region, false);
	}
	
	private void assertHyperlink(int mouseOffset, int linkStartOffset, int linkLength) {
		IHyperlink[] links = getHyperlinks(mouseOffset);
		assertNotNull(links);
		assertEquals(1, links.length);
		IRegion hyperlinkRegion = links[0].getHyperlinkRegion();
		assertEquals(linkStartOffset, hyperlinkRegion.getOffset());
		assertEquals(linkLength, hyperlinkRegion.getLength());
	}
	
	private void assertNotHyperlink(int mouseOffset) {
		IHyperlink[] links = getHyperlinks(mouseOffset);
		assertNull(links);
	}

	public void testHyperlinksCpp() throws Exception {
		// entire include highlighted
		setUpEditor(CPP_FILE_NAME_1, CPP_CODE, true);
		
		assertHyperlink(CPP_CODE.indexOf("#include") + 2, 0, "#include <stdio.h>".length()); 
		assertHyperlink(CPP_CODE.indexOf("<stdio.h>") + 2, 0, "#include <stdio.h>".length());
		assertHyperlink(CPP_CODE.indexOf("<stdio.h>") + "<stdio.h".length(), 0, "#include <stdio.h>".length());
		
		// hovering over the whitespace inside an include still results in a hyperlink
		assertHyperlink(CPP_CODE.indexOf("<stdio.h>") - 1, 0, "#include <stdio.h>".length());

		// no hyperlinks in macro bodies
		assertNotHyperlink(CPP_CODE.indexOf("#define") + 1);
		assertHyperlink(CPP_CODE.indexOf("SOMEMACRO"), CPP_CODE.indexOf("SOMEMACRO"), "SOMEMACRO".length());
		// see bug 259015
		assertHyperlink(CPP_CODE.indexOf("macro_token1"), CPP_CODE.indexOf("macro_token1"), "macro_token1".length());
		assertHyperlink(CPP_CODE.indexOf("macro_token2"), CPP_CODE.indexOf("macro_token2"), "macro_token2".length());
		// see bug 344604
		assertHyperlink(CPP_CODE.indexOf("arg);"), CPP_CODE.indexOf("arg);"), "arg".length());
		
		// no hyperlinks for comments
		assertNotHyperlink(CPP_CODE.indexOf("//") + 1);
		assertNotHyperlink(CPP_CODE.indexOf("COMMENT") + 1);
		
		// no hyperlinks for keywords
		assertNotHyperlink(CPP_CODE.indexOf("class") + 1); 
		assertNotHyperlink(CPP_CODE.indexOf("public") + 1); 
		assertNotHyperlink(CPP_CODE.indexOf("private") + 1); 
		assertNotHyperlink(CPP_CODE.indexOf("int x") + 1); 
		assertNotHyperlink(CPP_CODE.indexOf("char") + 1); 
		assertNotHyperlink(CPP_CODE.indexOf("void") + 1);
		
		// no hyperlinks for punctuation
		assertNotHyperlink(CPP_CODE.indexOf("{"));
		assertNotHyperlink(CPP_CODE.indexOf("}"));
		assertNotHyperlink(CPP_CODE.indexOf("(" + 1));
		assertNotHyperlink(CPP_CODE.indexOf(")"));
		assertNotHyperlink(CPP_CODE.indexOf(":"));
		assertNotHyperlink(CPP_CODE.indexOf(";"));
		
		// no hyperlinks inside strings
		assertNotHyperlink(CPP_CODE.indexOf("STRING") + 1);
		assertNotHyperlink(CPP_CODE.indexOf("STRING") + 6);
		assertNotHyperlink(CPP_CODE.indexOf("LITERAL") + 1);
		
		assertHyperlink(CPP_CODE.indexOf("Point {") + 1, CPP_CODE.indexOf("Point {"), "Point".length());
		assertHyperlink(CPP_CODE.indexOf("Point()") + 1, CPP_CODE.indexOf("Point()"), "Point".length());
		assertHyperlink(CPP_CODE.indexOf("~Point()") + 1, CPP_CODE.indexOf("~Point()"), "~Point".length());
		assertHyperlink(CPP_CODE.indexOf("set(") + 1, CPP_CODE.indexOf("set("), "set".length());
		assertHyperlink(CPP_CODE.indexOf("getX()") + 1, CPP_CODE.indexOf("getX()"), "getX".length());
		assertHyperlink(CPP_CODE.indexOf("getY()") + 1, CPP_CODE.indexOf("getY()"), "getY".length());
		
		// hyperlinks for overloaded operators
		assertHyperlink(CPP_CODE.indexOf("+ p"), CPP_CODE.indexOf("+ p"), 1);
	}

	public void testHyperlinksCppWithoutAST() throws Exception {
		// entire include highlighted
		setUpEditor(CPP_FILE_NAME_2, CPP_CODE, false);
		
		assertHyperlink(CPP_CODE.indexOf("#include") + 2, 0, "#include <stdio.h>".length()); 
		assertHyperlink(CPP_CODE.indexOf("<stdio.h>") + 2, 0, "#include <stdio.h>".length());
		assertHyperlink(CPP_CODE.indexOf("<stdio.h>") + "<stdio.h".length(), 0, "#include <stdio.h>".length());
		
		// hovering over the whitespace inside an include still results in a hyperlink
		assertHyperlink(CPP_CODE.indexOf("<stdio.h>") - 1, 0, "#include <stdio.h>".length());

		// no hyperlinks in macro bodies
		assertNotHyperlink(CPP_CODE.indexOf("#define") + 1);
		assertHyperlink(CPP_CODE.indexOf("SOMEMACRO"), CPP_CODE.indexOf("SOMEMACRO"), "SOMEMACRO".length());
		// see bug 259015
		assertHyperlink(CPP_CODE.indexOf("macro_token1"), CPP_CODE.indexOf("macro_token1"), "macro_token1".length());
		assertHyperlink(CPP_CODE.indexOf("macro_token2"), CPP_CODE.indexOf("macro_token2"), "macro_token2".length());
		// see bug 344604
		assertHyperlink(CPP_CODE.indexOf("arg);"), CPP_CODE.indexOf("arg);"), "arg".length());
		
		// no hyperlinks for comments
		assertNotHyperlink(CPP_CODE.indexOf("//") + 1);
		assertNotHyperlink(CPP_CODE.indexOf("COMMENT") + 1);
		
		// no hyperlinks for keywords
		assertNotHyperlink(CPP_CODE.indexOf("class") + 1); 
		assertNotHyperlink(CPP_CODE.indexOf("public") + 1); 
		assertNotHyperlink(CPP_CODE.indexOf("private") + 1); 
		assertNotHyperlink(CPP_CODE.indexOf("int x") + 1); 
		assertNotHyperlink(CPP_CODE.indexOf("char") + 1); 
		assertNotHyperlink(CPP_CODE.indexOf("void") + 1);
		
		// no hyperlinks for punctuation
		assertNotHyperlink(CPP_CODE.indexOf("{"));
		assertNotHyperlink(CPP_CODE.indexOf("}"));
		assertNotHyperlink(CPP_CODE.indexOf("(" + 1));
		assertNotHyperlink(CPP_CODE.indexOf(")"));
		assertNotHyperlink(CPP_CODE.indexOf(":"));
		assertNotHyperlink(CPP_CODE.indexOf(";"));
		
		// no hyperlinks inside strings
		assertNotHyperlink(CPP_CODE.indexOf("STRING") + 1);
		assertNotHyperlink(CPP_CODE.indexOf("STRING") + 6);
		assertNotHyperlink(CPP_CODE.indexOf("LITERAL") + 1);
		
		assertHyperlink(CPP_CODE.indexOf("Point {") + 1, CPP_CODE.indexOf("Point {"), "Point".length());
		assertHyperlink(CPP_CODE.indexOf("Point()") + 1, CPP_CODE.indexOf("Point()"), "Point".length());
		// No-AST hyperlink detection doesn't know that tilde is part of the destructor name. 
		assertHyperlink(CPP_CODE.indexOf("~Point()") + 1, CPP_CODE.indexOf("~Point()") + 1, "Point".length());
		assertHyperlink(CPP_CODE.indexOf("set(") + 1, CPP_CODE.indexOf("set("), "set".length());
		assertHyperlink(CPP_CODE.indexOf("getX()") + 1, CPP_CODE.indexOf("getX()"), "getX".length());
		assertHyperlink(CPP_CODE.indexOf("getY()") + 1, CPP_CODE.indexOf("getY()"), "getY".length());
		
		// Overloaded operators are not hyperlinked when AST is not available.
        //assertHyperlink(CPP_CODE.indexOf("+ p"), CPP_CODE.indexOf("+ p"), 1);
	}

	public void testHyperlinksCKeywords() throws Exception {
		setUpEditor(C_FILE_NAME_1, C_CODE_1, true);
		
		// 'class' is not a keyword in C, it should be hyperlinked
		assertHyperlink(C_CODE_1.indexOf("class") + 1, C_CODE_1.indexOf("class"), "class".length());
		
		// no hyperlinks for numeric literals
		assertNotHyperlink(C_CODE_1.indexOf("99") + 1);
	}

	public void testHyperlinksInactiveCode() throws Exception {
		setUpEditor(C_FILE_NAME_2, C_CODE_2, true);
		
		assertNotHyperlink(C_CODE_2.indexOf("#ifdef") + 2);
		assertNotHyperlink(C_CODE_2.indexOf("#else") + 2);
		assertNotHyperlink(C_CODE_2.indexOf("#endif") + 2);
		
		// see bug 259015
//		assertNotHyperlink(C_CODE_2.indexOf("nothere") + 1);
		assertHyperlink(C_CODE_2.indexOf("itworks") + 1, C_CODE_2.indexOf("itworks"), "itworks".length());
	}
}
