/*******************************************************************************
 * Copyright (c) 2012, 2014 Google, Inc 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:
 * 	   Sergey Prigogin (Google) - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.ui.tests.refactoring;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;

import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringContext;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptorProxy;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.RefactoringStatusEntry;
import org.eclipse.ltk.core.refactoring.history.RefactoringHistory;
import org.eclipse.ltk.internal.core.refactoring.history.RefactoringHistoryService;
import org.osgi.framework.Bundle;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.IPDOMManager;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.testplugin.CProjectHelper;
import org.eclipse.cdt.core.testplugin.TestScannerProvider;
import org.eclipse.cdt.core.testplugin.util.BaseTestCase;
import org.eclipse.cdt.core.testplugin.util.TestSourceReader;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.PreferenceConstants;
import org.eclipse.cdt.ui.testplugin.CTestPlugin;

import org.eclipse.cdt.internal.ui.refactoring.CRefactoring;
import org.eclipse.cdt.internal.ui.refactoring.CRefactoringContext;

/**
 * Common base for refactoring tests.
 */
public abstract class RefactoringTestBase extends BaseTestCase {
	private static final Pattern FILENAME_PATTERN = Pattern.compile("((\\w|_|-)+/)*(\\w|_|-)+\\.\\w+");
	/** Allows empty files to be created during test setup. */
	protected boolean createEmptyFiles = true;
	/** See {@link PreferenceConstants.CLASS_MEMBER_ASCENDING_VISIBILITY_ORDER} */
	protected boolean ascendingVisibilityOrder;
	/** Expected counts of errors, warnings and info messages */
	protected int expectedInitialErrors;
	protected int expectedInitialWarnings;
	protected int expectedFinalWarnings;
	protected int expectedFinalInfos;

	private boolean cpp = true;
	private ICProject cproject;
	private final Set<TestSourceFile> testFiles = new LinkedHashSet<>();
	private TestSourceFile selectedFile;
	private TextSelection selection;
	private TestSourceFile historyScript;

    protected RefactoringTestBase() {
		super();
	}

    protected RefactoringTestBase(String name) {
		super(name);
	}

	@Override
	public void setUp() throws Exception {
		super.setUp();
		resetPreferences();
		cproject = cpp ?
				CProjectHelper.createCCProject(getName() + System.currentTimeMillis(), "bin", IPDOMManager.ID_NO_INDEXER) :
				CProjectHelper.createCProject(getName() + System.currentTimeMillis(), "bin", IPDOMManager.ID_NO_INDEXER);
		TestScannerProvider.sLocalIncludes = new String[] { cproject.getProject().getLocation().toOSString() };

		Bundle bundle = CTestPlugin.getDefault().getBundle();
		CharSequence[] testData = TestSourceReader.getContentsForTest(bundle, "ui", getClass(), getName(), 0);

		for (CharSequence contents : testData) {
			TestSourceFile testFile = null;
			boolean firstAfterDelimiter = false;
			boolean expectedResult = false;
			BufferedReader reader = new BufferedReader(new StringReader(contents.toString()));
			String line;
			while ((line = reader.readLine()) != null) {
				String trimmedLine = line.trim();
				if (testFile == null) {
					if (isResultDelimiter(trimmedLine)) {
						expectedResult = true;
						firstAfterDelimiter = true;
						testFile = new TestSourceFile(null);
					} else {
						assertTrue("Invalid file name \"" + trimmedLine + "\"",
								FILENAME_PATTERN.matcher(trimmedLine).matches());
						testFile = new TestSourceFile(trimmedLine);
					}
				} else if (isResultDelimiter(trimmedLine)) {
					expectedResult = true;
					firstAfterDelimiter = true;
				} else if (expectedResult) {
					if (firstAfterDelimiter) {
						firstAfterDelimiter = false;
						if (FILENAME_PATTERN.matcher(trimmedLine).matches()) {
							testFile.setExpectedName(trimmedLine);
							continue;
						}
					}
					assertTrue(testFile.getExpectedName() != null);
					testFile.addLineToExpectedSource(line);
				} else {
					testFile.addLineToSource(line);
				}
			}
			reader.close();

			if (testFile.getName() != null && (createEmptyFiles || !testFile.getSource().isEmpty())) {
				TestSourceReader.createFile(cproject.getProject(), new Path(testFile.getName()),
						testFile.getSource());
			}
			testFiles.add(testFile);
			if (testFile.getName().endsWith(".xml")) {
				historyScript = testFile;
			} else if (selection == null) {
				selection = testFile.getSelection();
				if (selection != null)
					selectedFile = testFile;
			}
		}
		if (selectedFile == null && !testFiles.isEmpty()) {
			selectedFile = testFiles.iterator().next();
		}
		CCorePlugin.getIndexManager().setIndexerId(cproject, IPDOMManager.ID_FAST_INDEXER);
		waitForIndexer(cproject);
	}

	@Override
	public void tearDown() throws Exception {
		if (cproject != null) {
			cproject.getProject().delete(IResource.FORCE | IResource.ALWAYS_DELETE_PROJECT_CONTENT,
					npm());
		}
		resetPreferences();
		super.tearDown();
	}

	protected void assertRefactoringSuccess() throws Exception {
		executeRefactoring(true);
		compareFiles();
	}

	protected void assertRefactoringFailure() throws Exception {
		executeRefactoring(false);
	}

	protected void executeRefactoring(boolean expectedSuccess) throws Exception {
		if (ascendingVisibilityOrder) {
			getPreferenceStore().setValue(PreferenceConstants.CLASS_MEMBER_ASCENDING_VISIBILITY_ORDER,
					ascendingVisibilityOrder);
		}
		if (historyScript != null) {
			executeHistoryRefactoring(expectedSuccess);
			return;
		}

		Refactoring refactoring = createRefactoring();
		RefactoringContext context;
		if (refactoring instanceof CRefactoring) {
			context = new CRefactoringContext((CRefactoring) refactoring);
		} else {
			context = new RefactoringContext(refactoring);
		}
		executeRefactoring(refactoring, context, true, expectedSuccess);
	}

	protected void executeRefactoring(Refactoring refactoring, boolean expectedSuccess)
			throws CoreException, Exception {
		executeRefactoring(refactoring, null, false, expectedSuccess);
	}

	protected void executeRefactoring(Refactoring refactoring, RefactoringContext context,
			boolean withUserInput, boolean expectedSuccess) throws CoreException, Exception {
		try {
			RefactoringStatus initialStatus = refactoring.checkInitialConditions(npm());
			if (!expectedSuccess) {
				assertStatusFatalError(initialStatus);
				return;
			}
			if (expectedInitialErrors != 0) {
				assertStatusError(initialStatus, expectedInitialErrors);
			} else if (expectedInitialWarnings != 0) {
				assertStatusWarning(initialStatus, expectedInitialWarnings);
			} else {
				assertStatusOk(initialStatus);
			}

			if (withUserInput)
				simulateUserInput();
			RefactoringStatus finalStatus = refactoring.checkFinalConditions(npm());
			if (expectedFinalWarnings != 0) {
				assertStatusWarning(finalStatus, expectedFinalWarnings);
			} else if (expectedFinalInfos != 0) {
				assertStatusInfo(finalStatus, expectedFinalInfos);
			} else {
				assertStatusOk(finalStatus);
			}
			Change change = refactoring.createChange(npm());
			change.perform(npm());
		} finally {
			if (context != null)
				context.dispose();
		}
	}

	private void executeHistoryRefactoring(boolean expectedSuccess) throws Exception {
		URI uri= URIUtil.toURI(cproject.getProject().getLocation());
		String scriptSource = historyScript.getSource().replaceAll("\\$\\{projectPath\\}", uri.getPath());
		RefactoringHistory history = RefactoringHistoryService.getInstance().readRefactoringHistory(
				new ByteArrayInputStream(scriptSource.getBytes()), 0);
		for (RefactoringDescriptorProxy proxy : history.getDescriptors()) {
			RefactoringDescriptor descriptor = proxy.requestDescriptor(npm());
			RefactoringStatus status = new RefactoringStatus();
			RefactoringContext context = descriptor.createRefactoringContext(status);
			assertTrue(status.isOK());
			executeRefactoring(context.getRefactoring(), context, false, expectedSuccess);
		}
	}

	/**
	 * Creates a refactoring object.
	 */
	protected abstract Refactoring createRefactoring();

	/**
	 * Subclasses can override to simulate user input.
	 */
	protected void simulateUserInput() {
	}

	protected ICProject getCProject() {
		return cproject;
	}

	protected IProject getProject() {
		return cproject.getProject();
	}

	protected TestSourceFile getSelectedTestFile() {
		return selectedFile;
	}

	protected IFile getSelectedFile() {
		if (selectedFile == null)
			return null;
		return cproject.getProject().getFile(new Path(selectedFile.getName()));
	}

	protected ITranslationUnit getSelectedTranslationUnit() {
		IFile file = getSelectedFile();
		if (file == null)
			return null;
		return (ITranslationUnit) CoreModel.getDefault().create(file);
	}

	protected TestSourceFile getHistoryScriptFile() {
		return historyScript;
	}

	protected TextSelection getSelection() {
		return selection;
	}

	protected boolean isCpp() {
		return cpp;
	}

	protected void setCpp(boolean cpp) {
		this.cpp = cpp;
	}

	private boolean isResultDelimiter(String str) {
		if (str.isEmpty())
			return false;
		for (int i = 0; i < str.length(); i++) {
			if (str.charAt(i) != '=')
				return false;
		}
		return true;
	}

	protected void assertStatusOk(RefactoringStatus status) {
		if (!status.isOK())
			fail("Error or warning status: " + status.getEntries()[0].getMessage());
	}

	protected void assertStatusWarning(RefactoringStatus status, int number) {
		if (number > 0) {
			assertTrue("Warning status expected", status.hasWarning());
		}
		RefactoringStatusEntry[] entries = status.getEntries();
		int count = 0;
		for (RefactoringStatusEntry entry : entries) {
			if (entry.isWarning()) {
				++count;
			}
		}
		assertEquals("Found " + count + " warnings instead of expected " + number, number, count);
	}

	protected void assertStatusInfo(RefactoringStatus status, int number) {
		if (number > 0) {
			assertTrue("Info status expected", status.hasInfo());
		}
		RefactoringStatusEntry[] entries = status.getEntries();
		int count = 0;
		for (RefactoringStatusEntry entry : entries) {
			if (entry.isInfo()) {
				++count;
			}
		}
		assertEquals("Found " + count + " informational messages instead of expected " + number, number, count);
	}

	protected void assertStatusError(RefactoringStatus status, int number) {
		if (number > 0) {
			assertTrue("Error status expected", status.hasError());
		}
		RefactoringStatusEntry[] entries = status.getEntries();
		int count = 0;
		for (RefactoringStatusEntry entry : entries) {
			if (entry.isError()) {
				++count;
			}
		}
		assertEquals("Found " + count + " errors instead of expected " + number, number, count);
	}

	protected void assertStatusFatalError(RefactoringStatus status, int number) {
		if (number > 0) {
			assertTrue("Fatal error status expected", status.hasFatalError());
		}
		RefactoringStatusEntry[] entries = status.getEntries();
		int count = 0;
		for (RefactoringStatusEntry entry : entries) {
			if (entry.isFatalError()) {
				++count;
			}
		}
		assertEquals("Found " + count + " fatal errors instead of expected " + number, number, count);
	}

	protected void assertStatusFatalError(RefactoringStatus status) {
		assertTrue("Fatal error status expected", status.hasFatalError());
	}

	protected void assertEquals(TestSourceFile testFile, IFile file) throws Exception {
		String actualSource = getFileContents(file);
		assertEquals(testFile.getExpectedSource(), actualSource);
	}

	protected void compareFiles() throws Exception {
		for (TestSourceFile testFile : testFiles) {
			String expectedSource = testFile.getExpectedSource();
			IFile file = cproject.getProject().getFile(new Path(testFile.getExpectedName()));
			String actualSource = getFileContents(file);
			expectedSource= expectedSource.replace("\r\n", "\n");
			actualSource= actualSource.replace("\r\n", "\n");
			assertEquals(expectedSource, actualSource);
		}
	}

	protected String getFileContents(IFile file) throws Exception {
		BufferedReader reader = new BufferedReader(new InputStreamReader(file.getContents(), "UTF-8"));
		StringBuilder buffer = new StringBuilder();
		char[] part= new char[2048];
		int read= 0;
		while ((read= reader.read(part)) != -1)
			buffer.append(part, 0, read);
		reader.close();
		return buffer.toString().replace("\r", "");
	}

	protected void resetPreferences() {
		getPreferenceStore().setToDefault(PreferenceConstants.CLASS_MEMBER_ASCENDING_VISIBILITY_ORDER);
	}

	protected IPreferenceStore getPreferenceStore() {
		return CUIPlugin.getDefault().getPreferenceStore();
	}
}
