/*******************************************************************************
* Copyright (c) 2017 Benjamin Leipold 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:
*     Benjamin Leipold - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.tests.fieldassist;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import org.eclipse.jface.fieldassist.ContentProposalAdapter;
import org.eclipse.jface.fieldassist.IContentProposalProvider;
import org.eclipse.jface.fieldassist.SimpleContentProposalProvider;
import org.eclipse.jface.fieldassist.TextContentAdapter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.tests.harness.util.TestRunLogUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestWatcher;

public class ContentProposalAdapterTest {
	@Rule
	public TestWatcher LOG_TESTRUN = TestRunLogUtil.LOG_TESTRUN;

	/**
	 * A shell that hosts the decorated text control
	 */
	private Shell controlShell;

	/**
	 * Text control to be decorated by {@code contentProposalAdapter}
	 */
	private Text text;

	/**
	 * {@code ContentProposalAdapter} to test
	 */
	private ContentProposalAdapter contentProposalAdapter;

	/**
	 * Display of this test case.
	 */
	private Display display;

	/**
	 * {@code true} if {@code display} has to be disposed in {@link #tearDown()}
	 */
	private boolean disposeDisplay;

	/**
	 * The original number of shells at the beginning of the test.
	 */
	private int originalShellCount;

	/**
	 * bug 520372: ContentProposalAdapter with autoActivationDelay pops up although
	 * control has already lost focus
	 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=520372
	 *
	 * Tests whether no proposal popup was opened if TAB was pressed within
	 * activation delay.
	 *
	 * @throws Exception
	 */
	@Test
	public void testBug520372AutoActivationDelayTab() throws Exception {
		sendKeyDownToControl('o');
		sendKeyDownToControl(SWT.TAB);
		ensurePopupIsUp();

		assertOneShellUp();
	}

	/**
	 * bug 520372: ContentProposalAdapter with autoActivationDelay pops up although
	 * control has already lost focus
	 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=520372
	 *
	 * Tests whether no proposal popup was opened if CR was pressed within
	 * activation delay.
	 *
	 * @throws Exception
	 */
	@Test
	public void testBug520372AutoActivationDelayCR() throws Exception {
		sendKeyDownToControl('o');
		sendKeyDownToControl(SWT.CR);
		ensurePopupIsUp();

		assertOneShellUp();
	}

	/**
	 * bug 520372: ContentProposalAdapter with autoActivationDelay pops up although
	 * control has already lost focus
	 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=520372
	 *
	 * Tests whether no proposal popup was opened if ESC was pressed within
	 * activation delay.
	 *
	 * @throws Exception
	 */
	@Test
	public void testBug520372AutoActivationDelayESC() throws Exception {
		sendKeyDownToControl('o');
		sendKeyDownToControl(SWT.ESC);
		ensurePopupIsUp();

		assertOneShellUp();
	}

	// most of the following code is copied from AbstractFieldAssistTestCase

	@Before
	public final void setUp() throws Exception {
		Display display = getDisplay();
		originalShellCount = display.getShells().length;
		controlShell = new Shell(display);
		text = new Text(controlShell, SWT.SINGLE);
		controlShell.open();
		spinEventLoop();
		contentProposalAdapter = createContentProposalAdapter(this.text);
		assertNotNull(contentProposalAdapter);
	}

	@After
	public final void tearDown() throws Exception {
		if (controlShell != null) {
			spinEventLoop();
			controlShell.close();
		}
		if (display != null) {
			if (disposeDisplay) {
				display.dispose();
			}
			this.display = null;
		}
	}

	private Display getDisplay() {
		if (display == null) {
			Display newDisplay = Display.getCurrent();
			if (newDisplay == null) {
				newDisplay = new Display();
				disposeDisplay = true;
			}
			display = newDisplay;
		}
		return display;
	}

	/**
	 * Gives focus to the field assist control.
	 */
	private void sendFocusInToControl() {
		text.setFocus();
		spinEventLoop();
	}

	/**
	 * Sends an SWT KeyDown event for the specified character to the field assist
	 * control.
	 *
	 * @param character the character that has been pressed
	 */
	private void sendKeyDownToControl(char character) {
		// fake a KeyDown event
		sendFocusInToControl();
		Event event = new Event();
		event.type = SWT.KeyDown;
		event.character = character;
		assertTrue("unable to post event to display queue for test case", text.getDisplay().post(event));
		spinEventLoop();
	}

	private void spinEventLoop() {
		// spin the event loop again because we have some asyncExec calls in the
		// ContentProposalAdapter class

		Display disp = getDisplay();
		while (disp.readAndDispatch()) {
		}
	}

	private static ContentProposalAdapter createContentProposalAdapter(Control control) {
		ContentProposalAdapter contentProposalAdapter = new ContentProposalAdapter(control, new TextContentAdapter(),
				createContentProposalProvider(), null, null);
		contentProposalAdapter.setAutoActivationDelay(2000);
		return contentProposalAdapter;
	}

	private static IContentProposalProvider createContentProposalProvider() {
		return new SimpleContentProposalProvider(getProposals());
	}

	private static String[] getProposals() {
		return new String[] { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" };
	}

	private void ensurePopupIsUp() {
		// if our autoactivation delay is zero, we use an asyncExec to get the
		// popup up, hence, we need to spin the event loop
		if (contentProposalAdapter.getAutoActivationDelay() == 0) {
			spinEventLoop();
		} else {
			long time = System.currentTimeMillis();
			long target = time + contentProposalAdapter.getAutoActivationDelay();
			while (target > time) {
				spinEventLoop(); // remain responsive
				time = System.currentTimeMillis();
			}
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// nothing to do
			}
			spinEventLoop();
		}
	}

	/**
	 * Checks that there is only one shell up, the original field assist window.
	 */
	private void assertOneShellUp() {
		spinEventLoop();
		assertEquals("There should only be one shell up, the dialog", originalShellCount + 1,
				text.getDisplay().getShells().length);
	}
}
