/*******************************************************************************
 * Copyright (c) 2010, 2012 Broadcom Corporation 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:
 * James Blackburn (Broadcom Corp.)
 *******************************************************************************/

package org.eclipse.cdt.core.settings.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.testplugin.ResourceHelper;
import org.eclipse.cdt.core.testplugin.util.BaseTestCase;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;

import junit.framework.TestSuite;

/**
 * Class for testing exported settings and project references.
 * This functionality is provided by the CfgExportSettingContainerFactory which plugins
 * into the CExternalSettingsManager
 */
public class CConfigurationDescriptionExportSettings extends BaseTestCase {

	CoreModel coreModel = CoreModel.getDefault();

	public static TestSuite suite() {
		return suite(CConfigurationDescriptionExportSettings.class, "_");
	}

	// Setting entries 1
	final ICLanguageSettingEntry entries[] = new ICLanguageSettingEntry[] { new CMacroEntry("a", "b", 0),
			new CMacroEntry("c", "d", 0), new CIncludePathEntry("a/b/c", 0), new CIncludePathEntry("d/e/f", 0), };

	// Setting entries 2
	final ICLanguageSettingEntry entries2[] = new ICLanguageSettingEntry[] { new CMacroEntry("a2", "b2", 0),
			new CMacroEntry("c2", "d2", 0), new CIncludePathEntry("a/b/c/2", 0), new CIncludePathEntry("d/e/f/2", 0), };

	/**
	 * This tests for simple reference propagation.
	 * It used to live in the Managedbuild testsuite in ProjectModelTests.java
	 * but is moved here as it doesn't test any managedbuilder specific functionality
	 */
	public void testReferences() throws Exception {
		final String projectName4 = "test4";
		final String projectName5 = "test5";

		IProject project4 = ResourceHelper.createCDTProjectWithConfig(projectName4);
		IProject project5 = ResourceHelper.createCDTProjectWithConfig(projectName5);

		ICProjectDescription des4 = coreModel.getProjectDescription(project4);
		ICProjectDescription des5 = coreModel.getProjectDescription(project5);
		ICConfigurationDescription dess[] = des5.getConfigurations();

		dess[0].createExternalSetting(null, null, null, entries);
		dess[0].setActive();

		ICExternalSetting extSettings[] = dess[0].getExternalSettings();
		assertEquals(extSettings.length, 1);

		checkEquivContents(extSettings[0].getEntries(), entries);
		List<ICLanguageSettingEntry> list = new ArrayList<>(Arrays.asList(entries));
		list.remove(3);
		list.remove(2);
		checkEquivContents(extSettings[0].getEntries(ICSettingEntry.MACRO), list.toArray());
		list = new ArrayList<>(Arrays.asList(entries));
		list.remove(0);
		list.remove(0);
		checkEquivContents(extSettings[0].getEntries(ICSettingEntry.INCLUDE_PATH), list.toArray());
		coreModel.setProjectDescription(project5, des5);

		extSettings = coreModel.getProjectDescription(project5).getActiveConfiguration().getExternalSettings();
		assertEquals(extSettings.length, 1);

		checkEquivContents(extSettings[0].getEntries(), entries);
		list = new ArrayList<>(Arrays.asList(entries));
		list.remove(3);
		list.remove(2);
		checkEquivContents(extSettings[0].getEntries(ICSettingEntry.MACRO), list.toArray());
		list = new ArrayList<>(Arrays.asList(entries));
		list.remove(0);
		list.remove(0);
		checkEquivContents(extSettings[0].getEntries(ICSettingEntry.INCLUDE_PATH), list.toArray());

		dess = des4.getConfigurations();
		ICLanguageSetting ls = dess[0].getRootFolderDescription().getLanguageSettingForFile("a.c");
		ICLanguageSettingEntry macros[] = ls.getSettingEntries(ICSettingEntry.MACRO);
		ICLanguageSettingEntry includes[] = ls.getSettingEntries(ICSettingEntry.INCLUDE_PATH);
		assertFalse(Arrays.asList(macros).contains(entries[0]));
		assertFalse(Arrays.asList(macros).contains(entries[1]));
		assertFalse(Arrays.asList(includes).contains(entries[2]));
		assertFalse(Arrays.asList(includes).contains(entries[3]));
		Map<String, String> map = new HashMap<>();
		map.put(projectName5, "");
		dess[0].setReferenceInfo(map);
		ICLanguageSettingEntry updatedMacros[] = ls.getSettingEntries(ICSettingEntry.MACRO);
		ICLanguageSettingEntry udatedIncludes[] = ls.getSettingEntries(ICSettingEntry.INCLUDE_PATH);
		assertTrue(Arrays.asList(updatedMacros).contains(entries[0]));
		assertTrue(Arrays.asList(updatedMacros).contains(entries[1]));
		assertTrue(Arrays.asList(udatedIncludes).contains(entries[2]));
		assertTrue(Arrays.asList(udatedIncludes).contains(entries[3]));
	}

	/**
	 * This tests importing projects with references works even if the projects are
	 * imported in opposite order
	 * @throws Exception
	 */
	public void testProjectImport() throws Exception {
		final IProject libProj = ResourceHelper.createCDTProjectWithConfig("libProj");
		final IProject mainProj = ResourceHelper.createCDTProjectWithConfig("mainProj");

		// set the settings on the lib config; reference it from the main config
		{
			ICProjectDescription desLib = coreModel.getProjectDescription(libProj);
			ICProjectDescription desMain = coreModel.getProjectDescription(mainProj);
			ICConfigurationDescription cfgLib = desLib.getActiveConfiguration();
			ICConfigurationDescription cfgMain = desMain.getActiveConfiguration();

			cfgLib.createExternalSetting(null, null, null, entries);
			coreModel.setProjectDescription(libProj, desLib);

			// Main Project references lib project
			cfgMain.setReferenceInfo(new HashMap<String, String>() {
				{
					put(libProj.getName(), "");
				}
			});
			coreModel.setProjectDescription(mainProj, desMain);

			// Referenced settings have been picked up
			for (ICLanguageSettingEntry e : entries) {
				assertTrue(cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
						.getSettingEntriesList(e.getKind()).contains(e));
			}
		}

		// Now delete the two projects, import main first
		// then lib and check we're A-Ok
		libProj.delete(false, false, null);
		mainProj.delete(false, false, null);

		// project description obviously no longer eixsts
		assertNull(coreModel.getProjectDescription(mainProj));
		assertNull(coreModel.getProjectDescription(libProj));

		// Re-import the main project first
		mainProj.create(null);
		mainProj.open(null);

		// Now re-open the lib project
		assertFalse(libProj.exists());
		libProj.create(null);
		libProj.open(null);

		// Referenced settings should still exist
		ICConfigurationDescription cfgMain = coreModel.getProjectDescription(mainProj, false).getActiveConfiguration();
		ICConfigurationDescription cfgLib = coreModel.getProjectDescription(libProj, false).getActiveConfiguration();

		checkEquivContents(cfgLib.getExternalSettings()[0].getEntries(), entries);
		for (ICLanguageSettingEntry e : entries) {
			assertTrue(cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
		}
	}

	/**
	 * Tests that updating the exported settings on the active configuration, without updating the set
	 * of references, is correctly picked up in referencing projects.
	 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=312575
	 */
	public void testUpdateExportedSettingsActiveCfg() throws Exception {
		final IProject libProj = ResourceHelper.createCDTProjectWithConfig("libUpdateExtSettings");
		final IProject mainProj = ResourceHelper.createCDTProjectWithConfig("mainProjUpdateExtSettings");

		{
			// set the settings on the lib config; reference it from the main config
			ICProjectDescription desLib = coreModel.getProjectDescription(libProj);
			ICProjectDescription desMain = coreModel.getProjectDescription(mainProj);
			ICConfigurationDescription cfgLib = desLib.getActiveConfiguration();
			ICConfigurationDescription cfgMain = desMain.getActiveConfiguration();

			cfgLib.createExternalSetting(null, null, null, entries);
			coreModel.setProjectDescription(libProj, desLib);

			// Main Project references lib project
			cfgMain.setReferenceInfo(new HashMap<String, String>() {
				{
					put(libProj.getName(), "");
				}
			});
			coreModel.setProjectDescription(mainProj, desMain);

			// Referenced settings have been picked up
			for (ICLanguageSettingEntry e : entries) {
				assertTrue(cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
						.getSettingEntriesList(e.getKind()).contains(e));
			}
		}

		{
			// Now apply different language setting entries
			ICProjectDescription desLib = coreModel.getProjectDescription(libProj);
			ICConfigurationDescription cfgLib = desLib.getActiveConfiguration();
			cfgLib.removeExternalSetting(cfgLib.getExternalSettings()[0]);
			cfgLib.createExternalSetting(null, null, null, entries2);
			coreModel.setProjectDescription(libProj, desLib);

			// Check the exported settings is correct
			checkEquivContents(entries2, cfgLib.getExternalSettings()[0].getEntries());
			// Fetch the configuration a-fresh to pick up the settings
			ICConfigurationDescription cfgMain = coreModel.getProjectDescription(mainProj, false)
					.getActiveConfiguration();
			assertTrue(cfgMain.getReferenceInfo().containsKey(libProj.getName()));

			// Referenced settings have changed from entries -> entries2
			for (ICLanguageSettingEntry e : entries)
				assertTrue(!cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
						.getSettingEntriesList(e.getKind()).contains(e));
			for (ICLanguageSettingEntry e : entries2)
				assertTrue(cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
						.getSettingEntriesList(e.getKind()).contains(e));
		}
	}

	/**
	 * This is the same as testeUpdateExprtedSettingsActiveCfg except we explicitly name the
	 * referenced configuration.
	 * Tests that updating the exported settings on a name configuration, without updating the set
	 * of references, is correctly picked up in referencing projects
	 */
	public void testUpdateExportedSettingsNamedConfig() throws Exception {
		final IProject libProj = ResourceHelper.createCDTProjectWithConfig("libUpdateExpSettingsNamed");
		final IProject mainProj = ResourceHelper.createCDTProjectWithConfig("mainProjUpdateExpSettingsNamed");

		{
			// set the settings on the lib config; reference it from the main config
			ICProjectDescription desLib = coreModel.getProjectDescription(libProj);
			ICProjectDescription desMain = coreModel.getProjectDescription(mainProj);
			final ICConfigurationDescription cfgLib = desLib.getActiveConfiguration();
			ICConfigurationDescription cfgMain = desMain.getActiveConfiguration();

			cfgLib.createExternalSetting(null, null, null, entries);
			coreModel.setProjectDescription(libProj, desLib);

			// Main Project references lib project
			cfgMain.setReferenceInfo(new HashMap<String, String>() {
				{
					put(libProj.getName(), cfgLib.getId());
				}
			});
			coreModel.setProjectDescription(mainProj, desMain);

			// Referenced settings have been picked up
			for (ICLanguageSettingEntry e : entries) {
				assertTrue(cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
						.getSettingEntriesList(e.getKind()).contains(e));
			}
		}

		{
			// Now apply different language setting entries
			ICProjectDescription desLib = coreModel.getProjectDescription(libProj);
			ICConfigurationDescription cfgLib = desLib.getActiveConfiguration();
			cfgLib.removeExternalSetting(cfgLib.getExternalSettings()[0]);
			cfgLib.createExternalSetting(null, null, null, entries2);
			coreModel.setProjectDescription(libProj, desLib);

			// Check the exported settings is correct
			checkEquivContents(entries2, cfgLib.getExternalSettings()[0].getEntries());
			// Fetch the configuration a-fresh to pick up the settings
			ICConfigurationDescription cfgMain = coreModel.getProjectDescription(mainProj, false)
					.getActiveConfiguration();
			assertTrue(cfgMain.getReferenceInfo().get(libProj.getName()).equals(cfgLib.getId()));

			// Referenced settings have changed from entries -> entries2
			for (ICLanguageSettingEntry e : entries)
				assertTrue(!cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
						.getSettingEntriesList(e.getKind()).contains(e));
			for (ICLanguageSettingEntry e : entries2)
				assertTrue(cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
						.getSettingEntriesList(e.getKind()).contains(e));
		}
	}

	/**
	 * Bug 312575 tests that updating a .cproject external (e.g. via a repository update)
	 * causes referencing projects to correctly pick up changes to the project exports.
	 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=312575
	 */
	public void testExportedSettingsExternalUpdate() throws Exception {
		final IProject libProj = ResourceHelper.createCDTProjectWithConfig("libProj312575");
		final IProject mainProj = ResourceHelper.createCDTProjectWithConfig("mainProj312575");

		// .cproject file and its backup
		IFile libCproject = libProj.getFile(".cproject");
		IFile libCproject_back = libProj.getFile(".cproject_back");

		// set the settings on the lib config; reference it from the main config
		{
			ICProjectDescription desLib = coreModel.getProjectDescription(libProj);
			ICProjectDescription desMain = coreModel.getProjectDescription(mainProj);
			final ICConfigurationDescription cfgLib = desLib.getActiveConfiguration();
			ICConfigurationDescription cfgMain = desMain.getActiveConfiguration();

			cfgLib.createExternalSetting(null, null, null, entries);
			coreModel.setProjectDescription(libProj, desLib);

			// Main Project references lib project
			cfgMain.setReferenceInfo(new HashMap<String, String>() {
				{
					put(libProj.getName(), cfgLib.getId());
				}
			});
			coreModel.setProjectDescription(mainProj, desMain);
			checkEquivContents(entries, cfgLib.getExternalSettings()[0].getEntries());

			// Referenced settings have been picked up
			for (ICLanguageSettingEntry e : entries) {
				assertTrue(cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
						.getSettingEntriesList(e.getKind()).contains(e));
			}

			// Backup the .cproject
			libCproject_back.create(libCproject.getContents(), false, null);

			// Now apply different language setting entries
			cfgLib.removeExternalSetting(cfgLib.getExternalSettings()[0]);
			cfgLib.createExternalSetting(null, null, null, entries2);
			coreModel.setProjectDescription(libProj, desLib);
			checkEquivContents(entries2, cfgLib.getExternalSettings()[0].getEntries());
			// Referenced settings have been picked up
			for (ICLanguageSettingEntry e : entries2) {
				assertTrue(coreModel.getProjectDescription(mainProj).getActiveConfiguration().getRootFolderDescription()
						.getLanguageSettingForFile("a.c").getSettingEntriesList(e.getKind()).contains(e));
			}
		}

		// Now replace the .cproject with .cproject_back. The exported settings should be picked up in the referenced config
		libCproject.setContents(libCproject_back.getContents(), IResource.NONE, null);

		// Referenced settings should still exist
		ICConfigurationDescription cfgLib = coreModel.getProjectDescription(libProj, false).getActiveConfiguration();
		ICConfigurationDescription cfgMain = coreModel.getProjectDescription(mainProj, false).getActiveConfiguration();

		checkEquivContents(entries, cfgLib.getExternalSettings()[0].getEntries());
		// Referencing project contains entries and not entries2
		for (ICLanguageSettingEntry e : entries) {
			assertTrue(cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
		}
		for (ICLanguageSettingEntry e : entries2) {
			assertTrue(!cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
		}
	}

	/**
	 * Two configurations in two projects reference each other.
	 * Test that the system is happy with this and updates each correctly on
	 * the other's change.
	 */
	public void _testExportedSettingsCyclicExternalUpdate() throws Exception {
		final IProject libProj = ResourceHelper.createCDTProjectWithConfig("libProjCyclic");
		final IProject mainProj = ResourceHelper.createCDTProjectWithConfig("libProjCyclic2");

		// .cproject file and its backup
		final IFile libCproject = libProj.getFile(".cproject");
		final IFile libCproject_back = libProj.getFile(".cproject_back");
		final IFile mainCproject = mainProj.getFile(".cproject");
		final IFile mainCproject_back = mainProj.getFile(".cproject_back");

		// set the settings on the lib config; reference it from the main config & vice versa
		{
			ICProjectDescription desLib = coreModel.getProjectDescription(libProj);
			ICProjectDescription desMain = coreModel.getProjectDescription(mainProj);
			final ICConfigurationDescription cfgLib = desLib.getActiveConfiguration();
			final ICConfigurationDescription cfgMain = desMain.getActiveConfiguration();

			// Lib Exports entries
			cfgLib.createExternalSetting(null, null, null, entries);
			// Main Exports entries2
			cfgMain.createExternalSetting(null, null, null, entries2);

			// Main Project references lib project
			cfgMain.setReferenceInfo(new HashMap<String, String>() {
				{
					put(libProj.getName(), cfgLib.getId());
				}
			});
			cfgLib.setReferenceInfo(new HashMap<String, String>() {
				{
					put(mainProj.getName(), cfgMain.getId());
				}
			});
			coreModel.setProjectDescription(libProj, desLib);
			coreModel.setProjectDescription(mainProj, desMain);

			checkEquivContents(entries, cfgLib.getExternalSettings()[0].getEntries());
			checkEquivContents(entries2, cfgMain.getExternalSettings()[0].getEntries());

			// Backup the .cproject
			libCproject_back.create(libCproject.getContents(), false, null);
			mainCproject_back.create(mainCproject.getContents(), false, null);

			// Now apply different language setting entries
			cfgLib.removeExternalSetting(cfgLib.getExternalSettings()[0]);
			cfgLib.createExternalSetting(null, null, null, entries2);
			coreModel.setProjectDescription(libProj, desLib);
			cfgMain.removeExternalSetting(cfgMain.getExternalSettings()[0]);
			cfgMain.createExternalSetting(null, null, null, entries);
			coreModel.setProjectDescription(mainProj, desMain);

			checkEquivContents(entries2, cfgLib.getExternalSettings()[0].getEntries());
			checkEquivContents(entries, cfgMain.getExternalSettings()[0].getEntries());

		}
		ICConfigurationDescription cfgLib = coreModel.getProjectDescription(libProj).getActiveConfiguration();
		ICConfigurationDescription cfgMain = coreModel.getProjectDescription(mainProj).getActiveConfiguration();

		// Tests the exports are now the right way round
		for (ICLanguageSettingEntry e : entries) {
			assertTrue(!cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
			assertTrue(cfgLib.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
		}
		for (ICLanguageSettingEntry e : entries2) {
			assertTrue(cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
			assertTrue(!cfgLib.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
		}

		// Now replace the .cproject with .cproject_back. The exported settings should be picked up in the referenced config
		ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() {
			@Override
			public void run(IProgressMonitor monitor) throws CoreException {
				libCproject.setContents(libCproject_back.getContents(), IResource.NONE, null);
				mainCproject.setContents(mainCproject_back.getContents(), IResource.NONE, null);
			}
		}, null);

		// Referenced settings should still exist
		cfgLib = coreModel.getProjectDescription(libProj).getActiveConfiguration();
		cfgMain = coreModel.getProjectDescription(mainProj).getActiveConfiguration();

		checkEquivContents(entries, cfgLib.getExternalSettings()[0].getEntries());
		checkEquivContents(entries2, cfgMain.getExternalSettings()[0].getEntries());
		// Referencing project contains entries and not entries2
		for (ICLanguageSettingEntry e : entries) {
			assertTrue(cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
			assertTrue(!cfgLib.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
		}
		for (ICLanguageSettingEntry e : entries2) {
			assertTrue(!cfgMain.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
			assertTrue(cfgLib.getRootFolderDescription().getLanguageSettingForFile("a.c")
					.getSettingEntriesList(e.getKind()).contains(e));
		}
	}

	private void checkEquivContents(Object[] expected, Object[] actual) {
		if (expected == null) {
			assertTrue(actual == null);
			return;
		}
		assertTrue(actual != null);
		assertEquals(expected.length, actual.length);

		Set s1 = new HashSet(Arrays.asList(expected));
		Set s2 = new HashSet(Arrays.asList(actual));
		assertEquals(expected.length, s1.size());
		assertEquals(s1, s2);
	}

}
