/*******************************************************************************
 * Copyright (c) 2017, 2018 Red Hat Inc. 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:
 *     Lucas Bullen - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.tests.dialogs;

import java.io.ByteArrayInputStream;
import java.io.InputStream;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.text.Position;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.FilteredResourcesSelectionDialog;
import org.eclipse.ui.tests.harness.util.DisplayHelper;
import org.eclipse.ui.tests.harness.util.UITestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests that resources are highlighted to match user search input. See Bug
 * 519525, 520250, and 520251 for references.
 *
 * @since 3.14
 */
@RunWith(JUnit4.class)
public class ResourceItemLabelTest extends UITestCase {

	/**
	 * Constructs a new instance of <code>ResourceItemlLabelTest</code>.
	 */

	public ResourceItemLabelTest() {
		super(ResourceItemLabelTest.class.getSimpleName());
	}

	private IProject project;

	@Override
	protected void doSetUp() throws Exception {
		super.doSetUp();
		project = ResourcesPlugin.getWorkspace().getRoot()
				.getProject(getClass().getName() + "_" + System.currentTimeMillis());
		project.create(new NullProgressMonitor());
		project.open(new NullProgressMonitor());
	}

	/**
	 * Tests that the highlighting matches basic substrings
	 *
	 * @throws Exception
	 */
	@Test
	public void testSubstringMatch() throws Exception {
		Position[] atBeginning = { new Position(0, 2) };
		compareStyleRanges(atBeginning, getStyleRanges("te", "test.txt"), "test.txt", "");

		Position[] full = { new Position(0, 8) };
		compareStyleRanges(full, getStyleRanges("test.txt", "test.txt"), "test.txt", "");

		Position[] withDigits = { new Position(0, 3) };
		compareStyleRanges(withDigits, getStyleRanges("t3s", "t3st.txt"), "t3st.txt", "");
	}

	/**
	 * Tests that the highlighting matches basic substrings when infix search is
	 * automatically performed.
	 */
	@Test
	public void testSubstringMatch_withAutoInfix() throws Exception {
		Position[] atEnd = { new Position(2, 6) };
		compareStyleRanges(atEnd, getStyleRanges("st.txt", "test.txt"), "test.txt", "");

		Position[] atEndDifferentCase = { new Position(2, 6) };
		compareStyleRanges(atEndDifferentCase, getStyleRanges("ST.txt", "test.txt"), "test.txt", "");
	}

	/**
	 * Tests that the highlighting matches CamelCase searches
	 *
	 * @throws Exception
	 */
	@Test
	public void testCamelCaseMatch() throws Exception {
		Position[] atBeginning = { new Position(0, 1), new Position(4, 1) };
		compareStyleRanges(atBeginning, getStyleRanges("TT", "ThisTest.txt"), "ThisTest.txt", "");

		Position[] nextToEachother = { new Position(0, 1), new Position(4, 2) };
		compareStyleRanges(nextToEachother, getStyleRanges("TAT", "ThisATest.txt"), "ThisATest.txt", "");

		Position[] withSubstrings = { new Position(0, 2), new Position(4, 2) };
		compareStyleRanges(withSubstrings, getStyleRanges("ThTe", "ThisTest.txt"), "ThisTest.txt", "");

		Position[] withDigits = { new Position(0, 2), new Position(4, 2) };
		compareStyleRanges(withDigits, getStyleRanges("Th3T", "This3Test.txt"), "This3Test.txt", "");

		Position[] skippingDigit = { new Position(0, 2), new Position(5, 1) };
		compareStyleRanges(skippingDigit, getStyleRanges("ThT", "This3Test.txt"), "This3Test.txt", "");

	}

	/**
	 * Tests that the highlighting matches CamelCase searches when infix search is
	 * automatically performed.
	 */
	@Test
	public void testCamelCaseMatch_withAutoInfix() throws Exception {
		Position[] atEnd = { new Position(4, 1), new Position(6, 1) };
		compareStyleRanges(atEnd, getStyleRanges("IT", "ThisIsTest.txt"), "ThisIsTest.txt", "");
	}

	/**
	 * Tests that the highlighting matches searches using '*' and '?'
	 *
	 * @throws Exception
	 */
	@Test
	public void testPatternMatch() throws Exception {
		Position[] questionMark = { new Position(0, 1), new Position(2, 2) };
		compareStyleRanges(questionMark, getStyleRanges("t?st", "test.txt"), "test.txt", "");

		Position[] star = { new Position(0, 1), new Position(6, 2) };
		compareStyleRanges(star, getStyleRanges("t*xt", "test.txt"), "test.txt", "");

		Position[] both = { new Position(0, 1), new Position(2, 1), new Position(6, 2) };
		compareStyleRanges(both, getStyleRanges("t?s*xt", "test.txt"), "test.txt", "");

		Position[] withDigits = { new Position(0, 1), new Position(2, 2), new Position(7, 3) };
		compareStyleRanges(withDigits, getStyleRanges("t?s3*x3t", "tes3t.tx3t"), "tes3t.tx3t", "");
	}

	/**
	 * Tests that the highlighting matches searches using '*' and '?' when infix
	 * search is automatically performed.
	 */
	@Test
	public void testPatternMatch_withAutoInfix() throws Exception {
		Position[] atEnd = { new Position(1, 1), new Position(3, 2) };
		compareStyleRanges(atEnd, getStyleRanges("t?st", "atest.txt"), "atest.txt", "");
	}

	/**
	 * Tests that regex symbols do not break search
	 *
	 * @throws Exception
	 */
	@Test
	public void testBug529451() throws Exception {
		Position[] basic = { new Position(4, 1) };
		compareStyleRanges(basic, getStyleRanges("*$", "test$.txt"), "test$.txt", "");

		Position[] multiple = { new Position(0, 3), new Position(7, 6), new Position(14, 1) };
		compareStyleRanges(multiple, getStyleRanges("^${*}[])(+?-", "^${skip}[])(+s-"), "^${skip}[])(+s-", "");
	}

	/**
	 * Tests that the highlighting matches searches using '&gt;' and ' '
	 *
	 * @throws Exception
	 */
	@Test
	public void testDisableAutoPrefixMatching() throws Exception {
		Position[] questionMark = { new Position(0, 1), new Position(4, 4) };
		compareStyleRanges(questionMark, getStyleRanges("M*file<", "Makefile"), "Makefile", "");

		Position[] star = { new Position(0, 1), new Position(4, 4) };
		compareStyleRanges(star, getStyleRanges("M*file ", "MockFile"), "MockFile", "");

		Position[] both = { new Position(0, 3), new Position(6, 1) };
		compareStyleRanges(both, getStyleRanges("CreS<", "CreateStuff.java"), "CreateStuff.java", "");
	}

	/**
	 * Tests that the highlighting matches searches using '&lt;'.
	 */
	@Test
	public void testDisableAutoInfixMatching() throws Exception {
		Position[] substring = { new Position(0, 4) };
		compareStyleRanges(substring, getStyleRanges(">make", "Makefile"), "Makefile", "");

		Position[] pattern = { new Position(0, 1), new Position(4, 4) };
		compareStyleRanges(pattern, getStyleRanges(">M*file", "Makefile"), "Makefile", "");

		Position[] camelCase = { new Position(0, 3), new Position(6, 1) };
		compareStyleRanges(camelCase, getStyleRanges(">CreS", "CreateStuff.java"), "CreateStuff.java", "");
	}

	/**
	 * Tests that the highlighting matches extension searches
	 *
	 * @throws Exception
	 */
	@Test
	public void testExtensionMatch() throws Exception {
		Position[] basic = { new Position(8, 3) };
		compareStyleRanges(basic, getStyleRanges(".MF", "MANIFEST.MF"), "MANIFEST.MF", "");

		Position[] withSubstring = { new Position(0, 1), new Position(8, 3) };
		compareStyleRanges(withSubstring, getStyleRanges("M.MF", "MANIFEST.MF"), "MANIFEST.MF", "");

		Position[] withCamelCase = { new Position(4, 3), new Position(8, 1) };
		compareStyleRanges(withCamelCase, getStyleRanges(".TxT", "test.TxxT"), "test.TxxT", "");

		Position[] withPattern = { new Position(4, 2), new Position(8, 1) };
		compareStyleRanges(withPattern, getStyleRanges(".t*t", "test.txxt"), "test.txxt", "");

		Position[] withDigits = { new Position(4, 2), new Position(8, 1) };
		compareStyleRanges(withDigits, getStyleRanges(".3*3", "test.3xx3"), "test.3xx3", "");
	}

	/**
	 * Tests for Bug 528301: Camel Case match with precursing letter matches
	 *
	 * @throws Exception
	 */
	@Test
	public void testBug528301() throws Exception {
		Position[] withSameLettersBeforeCamel = { new Position(0, 1), new Position(3, 1), new Position(5, 1) };
		compareStyleRanges(withSameLettersBeforeCamel, getStyleRanges("ABC", "AbcBzCz.txt"), "AbcBzCz.txt", "");

		Position[] withDigits = { new Position(0, 1), new Position(4, 1), new Position(6, 1), new Position(8, 1) };
		compareStyleRanges(withDigits, getStyleRanges("AB5C", "Ab5cBz5zCz.txt"), "Ab5cBz5zCz.txt", "");
	}

	@Test
	public void testBug528301_withConsecutiveWildcards() throws Exception {
		Position[] questionMarkStar = { new Position(0, 1), new Position(5, 2) };
		compareStyleRanges(questionMarkStar, getStyleRanges("a?*fg", "abcdefg.txt"), "abcdefg.txt", "");

		Position[] starQuestionMark = { new Position(0, 1), new Position(5, 2) };
		compareStyleRanges(starQuestionMark, getStyleRanges("a*?fg", "abcdefg.txt"), "abcdefg.txt", "");
	}

	/**
	 * Tests for Bug 531610: Open Resource dialog doesn't show paths for duplicated
	 * files
	 *
	 * @throws Exception
	 */
	@Test
	public void testBug531610() throws Exception {
		IFolder folder = project.getFolder("folder");
		IFile fileB = folder.getFile("file");
		folder.create(true, true, new NullProgressMonitor());
		fileB.create(stream, true, new NullProgressMonitor());
		StyleRange[] ranges = getStyleRanges("file", "file");
		// if true adds "/folder" length to the range
		boolean withFolder = false;
		for (StyleRange range : ranges) {
			if (range.length > 3 + project.getName().length()) {
				withFolder = true;
			}
		}
		Position[] withDigits = { new Position(0, 4) }; // " - "
		// withFolder - "/folder"
		compareStyleRanges(withDigits, ranges, "file", withFolder ? "/folder" : "");
	}

	/**
	 * Test for Bug 566858 - java.lang.StringIndexOutOfBoundsException in
	 * FilteredResourcesSelectionDialog with leading whitespace in resource names
	 */
	@Test
	public void testBug566858_leadingWhitespace() throws Exception {
		Position[] pos = new Position[] { new Position(0, 5) };
		compareStyleRanges(pos, getStyleRanges(" test", " test.txt"), " test.txt", "");

		pos = new Position[] { new Position(1, 4) };
		compareStyleRanges(pos, getStyleRanges("?test", " test.txt"), " test.txt", "");

		pos = new Position[] { new Position(5, 4) };
		compareStyleRanges(pos, getStyleRanges("*.txt", " test.txt"), " test.txt", "");
	}

	private void compareStyleRanges(Position[] expected, StyleRange[] actual, String fileName, String fileParentPath) {
		assertEquals("Length of StyleRanges is incorrect: " + printStyleRanges(actual), expected.length + 1,
				actual.length);
		int i;
		for (i = 0; i < actual.length - 1; i++) {
			assertEquals("Start of StyleRange at index " + i + " is incorrect.", expected[i].offset, actual[i].start);
			assertEquals("Length of StyleRange at index " + i + " is incorrect.", expected[i].length, actual[i].length);
		}
		assertEquals("Start of file path StyleRange is incorrect.", fileName.length(), actual[i].start);
		assertEquals("Length of file path StyleRange at index is incorrect.",
				3 + project.getName().length() + fileParentPath.length(), actual[i].length);
	}

	private FilteredResourcesSelectionDialog dialog;
	private static InputStream stream = new ByteArrayInputStream(new byte[0]);

	private StyleRange[] getStyleRanges(String searchString, String fileName) throws Exception {
		IFile file = project.getFile(fileName);
		file.create(stream, true, new NullProgressMonitor());
		project.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());

		dialog = new FilteredResourcesSelectionDialog(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
				true, project,
				IResource.FILE);
		dialog.setBlockOnOpen(false);
		dialog.create();
		dialog.open();
		Shell shell = dialog.getShell();

		new DisplayHelper() {
			@Override
			protected boolean condition() {
				return project.getFile(fileName).exists();
			}
		}.waitForCondition(shell.getDisplay(), 1000);

		assertTrue("File was not created", project.getFile(fileName).exists());
		dialog.reloadCache(true, new NullProgressMonitor());

		((Text) dialog.getPatternControl()).setText(searchString);
		Table table = (Table) ((Composite) ((Composite) ((Composite) shell.getChildren()[0]).getChildren()[0])
				.getChildren()[0]).getChildren()[3];

		new DisplayHelper() {
			@Override
			protected boolean condition() {
				return table.getItemCount() > 0;
			}
		}.waitForCondition(shell.getDisplay(), 3000);

		Object data = table.getItem(0).getData("org.eclipse.jfacestyled_label_key_0");

		dialog.close();
		file.delete(true, null);
		if (data == null || !(data instanceof StyleRange[])) {
			fail("No StyleRanges found for the TableItem");
		}

		return (StyleRange[]) data;
	}

	private String printStyleRanges(StyleRange[] styleRanges) {
		if (styleRanges == null) {
			return "null";
		}
		if (styleRanges.length == 0) {
			return "[]";
		}
		StringBuilder builder = new StringBuilder();
		builder.append('[');
		for (StyleRange range : styleRanges) {
			builder.append("{start: ");
			builder.append(range.start);
			builder.append(", length: ");
			builder.append(range.length);
			builder.append("}, ");
		}
		builder.setLength(builder.length() - 2);
		builder.append(']');
		return builder.toString();
	}

	@Override
	protected void doTearDown() throws Exception {
		if (dialog != null) {
			dialog.close();
		}
		if (project != null) {
			project.delete(true, null);
		}
		super.doTearDown();
	}
}
