/*******************************************************************************
 * Copyright (c) 2004, 2014 IBM 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.tests.services.datalocation;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.tests.harness.CoreTest;
import org.eclipse.equinox.log.SynchronousLogListener;
import org.eclipse.osgi.internal.framework.EquinoxConfiguration;
import org.eclipse.osgi.internal.location.EquinoxLocations;
import org.eclipse.osgi.launch.Equinox;
import org.eclipse.osgi.service.datalocation.Location;
import org.eclipse.osgi.tests.OSGiTestsActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.log.LogEntry;
import org.osgi.service.log.LogListener;
import org.osgi.service.log.LogReaderService;
import org.osgi.util.tracker.ServiceTracker;

public class BasicLocationTests extends CoreTest {

	String prefix = "";
	boolean windows = Platform.getOS().equals(Platform.OS_WIN32);
	ServiceTracker<Location, Location> configLocationTracker = null;
	ServiceTracker<Location, Location> instanceLocationTracker = null;

	public BasicLocationTests(String name) {
		super(name);
	}

	public static Test suite() {
		return new TestSuite(BasicLocationTests.class);
	}

	protected void setUp() throws Exception {
		super.setUp();

		prefix = windows ? "c:" : "";

		configLocationTracker = new ServiceTracker<>(OSGiTestsActivator.getContext(), OSGiTestsActivator.getContext().createFilter(Location.CONFIGURATION_FILTER), null);
		instanceLocationTracker = new ServiceTracker<>(OSGiTestsActivator.getContext(), OSGiTestsActivator.getContext().createFilter(Location.INSTANCE_FILTER), null);

		configLocationTracker.open();
		instanceLocationTracker.open();
	}

	protected void tearDown() throws Exception {
		configLocationTracker.close();
		instanceLocationTracker.close();
		super.tearDown();
	}

	private void checkSlashes(Map<String, Location> locations) {
		checkLocation(locations.get(Location.USER_FILTER), true, true, null);
		checkLocation(locations.get(Location.INSTANCE_FILTER), true, true, null);
		checkLocation(locations.get(Location.CONFIGURATION_FILTER), true, true, null);
		checkLocation(locations.get(Location.INSTALL_FILTER), true, true, null);
	}

	private void checkLocation(Location location, boolean leading, boolean trailing, String scheme) {
		if (location == null)
			return;
		URL url = location.getURL();
		if (scheme != null)
			assertEquals(scheme, url.getProtocol());
		if (!url.getProtocol().equals("file"))
			return;
		assertTrue(url.toExternalForm() + " should " + (trailing ? "" : "not") + " have a trailing slash", url.getFile().endsWith("/") == trailing);
		if (windows)
			assertTrue(url.toExternalForm() + " should " + (leading ? "" : "not") + " have a leading slash", url.getFile().startsWith("/") == leading);
	}

	public void testCreateLocation01() {
		Location configLocation = configLocationTracker.getService();
		File testLocationFile = OSGiTestsActivator.getContext().getDataFile("testLocations/testCreateLocation01");
		Location testLocation = configLocation.createLocation(null, null, false);
		try {
			testLocation.set(testLocationFile.toURL(), false);
		} catch (Throwable t) {
			fail("Failed to set location", t);
		}
		try {
			assertTrue("Could not lock location", testLocation.lock());
		} catch (IOException e) {
			fail("Failed to lock location", e);
		}
		testLocation.release();
	}

	public void testCreateLocation02() {
		Location configLocation = configLocationTracker.getService();
		File testLocationFile = OSGiTestsActivator.getContext().getDataFile("testLocations/testCreateLocation02");
		Location testLocation = configLocation.createLocation(null, null, true);
		try {
			testLocation.set(testLocationFile.toURL(), false);
		} catch (Throwable t) {
			fail("Failed to set location", t);
		}
		try {
			assertTrue("Could not lock location", testLocation.lock());
			testLocation.release();
			fail("Should not be able to lock read-only location");
		} catch (IOException e) {
			// expected
		}
	}

	public void testCreateLocation03() {
		Location configLocation = configLocationTracker.getService();
		File testLocationFile = OSGiTestsActivator.getContext().getDataFile("testLocations/testCreateLocation03");
		Location testLocation = configLocation.createLocation(null, null, false);
		try {
			testLocation.set(testLocationFile.toURL(), true);
		} catch (Throwable t) {
			fail("Failed to set location", t);
		}
		try {
			assertTrue("Could not lock location", testLocation.isLocked());
		} catch (IOException e) {
			fail("Failed to lock location", e);
		}
		testLocation.release();
	}

	public void testCreateLocation04() {
		Location configLocation = configLocationTracker.getService();
		File testLocationFile = OSGiTestsActivator.getContext().getDataFile("testLocations/testCreateLocation04");
		Location testLocation = configLocation.createLocation(null, null, true);
		try {
			testLocation.set(testLocationFile.toURL(), true);
			testLocation.release();
			fail("Should not be able to lock read-only location");
		} catch (Throwable t) {
			// expected
		}
	}

	public void testCreateLocation05() {
		Location configLocation = configLocationTracker.getService();
		File testLocationFile = OSGiTestsActivator.getContext().getDataFile("testLocations/testCreateLocation01");
		Location testLocation = configLocation.createLocation(null, null, false);
		try {
			testLocation.set(testLocationFile.toURL(), false);
		} catch (Throwable t) {
			fail("Failed to set location", t);
		}
		try {
			assertTrue("Could not lock location", testLocation.lock());
			assertFalse("Could lock a secend time", testLocation.lock());
			assertFalse("Could lock a third time", testLocation.lock());
		} catch (IOException e) {
			fail("Failed to lock location", e);
		} finally {
			testLocation.release();
		}
		try {
			assertTrue("Could not lock location", testLocation.lock());
		} catch (IOException e) {
			fail("Failed to lock location", e);
		} finally {
			testLocation.release();
		}
	}

	private static final String INSTANCE_DATA_AREA_PREFIX = ".metadata/.plugins/"; //$NON-NLS-1$

	public void testLocationDataArea01() {
		Location instance = instanceLocationTracker.getService();
		doAllTestLocationDataArea(instance, INSTANCE_DATA_AREA_PREFIX);

		Location configuration = configLocationTracker.getService();
		doAllTestLocationDataArea(configuration, "");
	}

	private void doAllTestLocationDataArea(Location location, String dataAreaPrefix) {
		doTestLocateDataArea(location, dataAreaPrefix, getName());
		doTestLocateDataArea(location, dataAreaPrefix, "");
		doTestLocateDataArea(location, dataAreaPrefix, "test/multiple/paths");
		doTestLocateDataArea(location, dataAreaPrefix, "test/multiple/../paths");
		doTestLocateDataArea(location, dataAreaPrefix, "test\\multiple\\paths");
		doTestLocateDataArea(location, dataAreaPrefix, "/test/begin/slash");

		File testLocationFile = OSGiTestsActivator.getContext().getDataFile("testLocations/" + getName());
		Location createdLocation = location.createLocation(null, null, false);
		try {
			createdLocation.set(testLocationFile.toURL(), false);
		} catch (Exception e) {
			fail("Failed to set location", e);
		}
		doTestLocateDataArea(createdLocation, dataAreaPrefix, getName());
		doTestLocateDataArea(createdLocation, dataAreaPrefix, "");
		doTestLocateDataArea(createdLocation, dataAreaPrefix, "test/multiple/paths");
		doTestLocateDataArea(createdLocation, dataAreaPrefix, "test/multiple/../paths");
		doTestLocateDataArea(location, dataAreaPrefix, "test\\multiple\\paths");
		doTestLocateDataArea(location, dataAreaPrefix, "/test/begin/slash");

		createdLocation = location.createLocation(null, null, false);
		try {
			createdLocation.getDataArea("shouldFail");
			fail("expected failure when location is not set");
		} catch (IOException e) {
			// expected;
		}
	}

	private void doTestLocateDataArea(Location location, String dataAreaPrefix, String namespace) {
		assertTrue("Location is not set", location.isSet());
		URL dataArea = null;
		try {
			dataArea = location.getDataArea(namespace);
		} catch (IOException e) {
			fail("Failed to get data area.", e);
		}
		assertNotNull("Data area is null.", dataArea);

		namespace = namespace.replace('\\', '/');
		if (namespace.startsWith("/")) {
			namespace = namespace.substring(1);
		}
		assertTrue("Data area is not the expected value: " + dataArea.toExternalForm(), dataArea.toExternalForm().endsWith(dataAreaPrefix + namespace));
	}

	public void testSetLocationWithEmptyLockFile() {
		Location configLocation = configLocationTracker.getService();
		File testLocationFile = OSGiTestsActivator.getContext().getDataFile("testLocations/testSetLocationWithEmptyLockFile"); //$NON-NLS-1$
		Location testLocation = configLocation.createLocation(null, null, false);
		try {
			testLocation.set(testLocationFile.toURL(), true, ""); //$NON-NLS-1$
			// Make sure it has created the default lock file
			File lockFile = new File(testLocationFile, ".metadata/.lock"); //$NON-NLS-1$
			assertTrue("Lock file does not exist!", lockFile.exists()); //$NON-NLS-1$
		} catch (Throwable t) {
			fail("Failed to set location", t); //$NON-NLS-1$
		}
		try {
			assertTrue("Could not lock location", testLocation.isLocked()); //$NON-NLS-1$
		} catch (IOException e) {
			fail("Failed to lock location", e); //$NON-NLS-1$
		}
		testLocation.release();
	}

	public void testSetLocationWithRelLockFile() {
		Location configLocation = configLocationTracker.getService();
		File testLocationFile = OSGiTestsActivator.getContext().getDataFile("testLocations/testSetLocationWithRelLockFile"); //$NON-NLS-1$
		Location testLocation = configLocation.createLocation(null, null, false);
		try {
			testLocation.set(testLocationFile.toURL(), true, ".mocklock"); //$NON-NLS-1$
			File lockFile = new File(testLocationFile, ".mocklock"); //$NON-NLS-1$
			assertTrue("Lock file does not exist!", lockFile.exists()); //$NON-NLS-1$
		} catch (Throwable t) {
			fail("Failed to set location", t); //$NON-NLS-1$
		}
		try {
			assertTrue("Could not lock location", testLocation.isLocked()); //$NON-NLS-1$
		} catch (IOException e) {
			fail("Failed to lock location", e); //$NON-NLS-1$
		}
		testLocation.release();
	}

	public void testSetLocationWithAbsLockFile() {
		Location configLocation = configLocationTracker.getService();
		File testLocationFile = OSGiTestsActivator.getContext().getDataFile("testLocations/testSetLocationWithAbsLockFile"); //$NON-NLS-1$
		File testLocationLockFile = OSGiTestsActivator.getContext().getDataFile("testLocations/mock.lock"); //$NON-NLS-1$
		assertTrue(testLocationLockFile.isAbsolute());
		Location testLocation = configLocation.createLocation(null, null, false);
		try {
			testLocation.set(testLocationFile.toURL(), true, testLocationLockFile.getAbsolutePath());
			assertTrue("The lock file should be present!", testLocationLockFile.exists()); //$NON-NLS-1$
		} catch (Throwable t) {
			fail("Failed to set location", t); //$NON-NLS-1$
		}
		try {
			assertTrue("Could not lock location", testLocation.isLocked()); //$NON-NLS-1$
		} catch (IOException e) {
			fail("Failed to lock location", e); //$NON-NLS-1$
		}
		testLocation.release();
		assertTrue("The lock file could not be removed!", testLocationLockFile.delete()); //$NON-NLS-1$
	}

	public void testSlashes() throws BundleException, InvalidSyntaxException {
		Map<String, String> fwkConfig = new HashMap<>();
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA + EquinoxLocations.READ_ONLY_AREA_SUFFIX, "true");
		fwkConfig.put(EquinoxLocations.PROP_USER_AREA, prefix + "/a");
		fwkConfig.put(EquinoxLocations.PROP_INSTANCE_AREA, prefix + "/c/d");
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA, prefix + "/e/f");
		fwkConfig.put(EquinoxLocations.PROP_INSTALL_AREA, "file:" + prefix + "/g");

		Equinox equinox = new Equinox(fwkConfig);
		equinox.init();
		try {
			checkSlashes(getLocations(equinox));
		} finally {
			equinox.stop();
		}

	}

	private static Map<String, Location> getLocations(Equinox equinox) throws InvalidSyntaxException {
		Map<String, Location> locations = new HashMap<>();
		BundleContext context = equinox.getBundleContext();
		addLocation(context, Location.CONFIGURATION_FILTER, locations);
		addLocation(context, Location.INSTALL_FILTER, locations);
		addLocation(context, Location.INSTANCE_FILTER, locations);
		addLocation(context, Location.USER_FILTER, locations);
		return locations;
	}

	private static void addLocation(BundleContext context, String filter, Map<String, Location> locations) throws InvalidSyntaxException {
		Collection<ServiceReference<Location>> locationRefs = context.getServiceReferences(Location.class, filter);
		if (!locationRefs.isEmpty()) {
			locations.put(filter, context.getService(locationRefs.iterator().next()));
		}
	}

	public void testSchemes() throws Exception {
		Map<String, String> fwkConfig = new HashMap<>();
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA + EquinoxLocations.READ_ONLY_AREA_SUFFIX, "true");
		fwkConfig.put(EquinoxLocations.PROP_USER_AREA, "http://example.com/a");
		fwkConfig.put(EquinoxLocations.PROP_INSTANCE_AREA, "ftp://example.com/c/d");
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA, "platform:/base/e/f");
		fwkConfig.put(EquinoxLocations.PROP_INSTALL_AREA, "file:" + prefix + "/g");

		Equinox equinox = new Equinox(fwkConfig);
		equinox.init();
		try {
			Map<String, Location> locations = getLocations(equinox);
			checkSlashes(locations);
			checkLocation(locations.get(Location.USER_FILTER), true, true, "http");
			checkLocation(locations.get(Location.INSTANCE_FILTER), true, true, "ftp");
			checkLocation(locations.get(Location.CONFIGURATION_FILTER), true, true, "platform");
			checkLocation(locations.get(Location.INSTALL_FILTER), true, true, "file");
		} finally {
			equinox.stop();
		}
	}

	public void testNone() throws Exception {
		Map<String, String> fwkConfig = new HashMap<>();
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA + EquinoxLocations.READ_ONLY_AREA_SUFFIX, "true");
		fwkConfig.put(EquinoxLocations.PROP_USER_AREA, "@none");
		fwkConfig.put(EquinoxLocations.PROP_INSTANCE_AREA, "@none");
		// TODO framework does not handle no config area well
		// fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA, "@none");
		fwkConfig.put(EquinoxLocations.PROP_INSTALL_AREA, "file:" + prefix + "/g");

		Equinox equinox = new Equinox(fwkConfig);
		equinox.init();
		try {
			Map<String, Location> locations = getLocations(equinox);
			assertNull("User location should be null", locations.get(Location.USER_FILTER));
			assertNull("Instance location should be null", locations.get(Location.INSTANCE_FILTER));
			// TODO assertNull("Configuration location should be null", locations.get(Location.CONFIGURATION_FILTER));
		} finally {
			equinox.stop();
		}
	}

	public void testNoDefault() throws Exception {
		Map<String, String> fwkConfig = new HashMap<>();
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA + EquinoxLocations.READ_ONLY_AREA_SUFFIX, "true");
		fwkConfig.put(EquinoxLocations.PROP_INSTALL_AREA, "file:" + prefix + "/g");
		fwkConfig.put(EquinoxLocations.PROP_INSTANCE_AREA, "@noDefault");
		fwkConfig.put(EquinoxLocations.PROP_USER_AREA, "@noDefault");

		Equinox equinox = new Equinox(fwkConfig);
		equinox.init();
		try {
			Map<String, Location> locations = getLocations(equinox);
			Location userLocation = locations.get(Location.USER_FILTER);
			Location instanceLocation = locations.get(Location.INSTANCE_FILTER);
			assertNull("User locatoin is not null.", userLocation.getURL());
			assertNull("Instance location is not null.", instanceLocation.getURL());
		} finally {
			equinox.stop();
		}
	}

	public void testUserDir() throws Exception {
		Map<String, String> fwkConfig = new HashMap<>();
		fwkConfig.put(EquinoxLocations.PROP_USER_AREA, "@user.dir");
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA + EquinoxLocations.READ_ONLY_AREA_SUFFIX, "true");
		fwkConfig.put(EquinoxLocations.PROP_INSTANCE_AREA, "@user.dir");
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA, "@user.dir");
		fwkConfig.put(EquinoxLocations.PROP_INSTALL_AREA, "file:" + prefix + "/g");

		Equinox equinox = new Equinox(fwkConfig);
		equinox.init();
		try {
			Map<String, Location> locations = getLocations(equinox);
			checkLocation(locations.get(Location.USER_FILTER), true, true, "file");
			checkLocation(locations.get(Location.INSTANCE_FILTER), true, true, "file");
			checkLocation(locations.get(Location.CONFIGURATION_FILTER), true, true, "file");
			checkLocation(locations.get(Location.INSTALL_FILTER), true, true, "file");
		} finally {
			equinox.stop();
		}
	}

	public void testUserHome() throws Exception {
		Map<String, String> fwkConfig = new HashMap<>();
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA + EquinoxLocations.READ_ONLY_AREA_SUFFIX, "true");
		fwkConfig.put(EquinoxLocations.PROP_USER_AREA, "@user.home");
		fwkConfig.put(EquinoxLocations.PROP_INSTANCE_AREA, "@user.home");
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA, "@user.home");
		fwkConfig.put(EquinoxLocations.PROP_INSTALL_AREA, "file:" + prefix + "/g");

		Equinox equinox = new Equinox(fwkConfig);
		equinox.init();
		try {
			Map<String, Location> locations = getLocations(equinox);
			checkLocation(locations.get(Location.USER_FILTER), true, true, "file");
			checkLocation(locations.get(Location.INSTANCE_FILTER), true, true, "file");
			checkLocation(locations.get(Location.CONFIGURATION_FILTER), true, true, "file");
			checkLocation(locations.get(Location.INSTALL_FILTER), true, true, "file");
		} finally {
			equinox.stop();
		}

	}

	public void testUNC() throws Exception {
		if (!windows)
			return;
		Map<String, String> fwkConfig = new HashMap<>();
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA + EquinoxLocations.READ_ONLY_AREA_SUFFIX, "true");
		fwkConfig.put(EquinoxLocations.PROP_USER_AREA, "//server/share/a");
		fwkConfig.put(EquinoxLocations.PROP_INSTANCE_AREA, "//server/share/b");
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA, "//server/share/c");
		fwkConfig.put(EquinoxLocations.PROP_INSTALL_AREA, "file://server/share/g");

		Equinox equinox = new Equinox(fwkConfig);
		equinox.init();
		try {
			Map<String, Location> locations = getLocations(equinox);
			checkLocation(locations.get(Location.USER_FILTER), true, true, "file");
			checkLocation(locations.get(Location.INSTANCE_FILTER), true, true, "file");
			checkLocation(locations.get(Location.CONFIGURATION_FILTER), true, true, "file");
			checkLocation(locations.get(Location.INSTALL_FILTER), true, true, "file");
		} finally {
			equinox.stop();
		}
	}

	public void testDebugLogOnGetURL() throws Exception {
		Properties debugOptions = new Properties();
		debugOptions.put("org.eclipse.osgi/debug/location", "true");
		File debugOptionsFile = OSGiTestsActivator.getContext().getDataFile(getName() + ".options");
		debugOptions.store(new FileOutputStream(debugOptionsFile), getName());
		Map<String, String> fwkConfig = new HashMap<>();
		fwkConfig.put(EquinoxLocations.PROP_CONFIG_AREA + EquinoxLocations.READ_ONLY_AREA_SUFFIX, "true");
		fwkConfig.put(EquinoxLocations.PROP_INSTALL_AREA, "file:" + prefix + "/g");
		fwkConfig.put(EquinoxConfiguration.PROP_DEBUG, debugOptionsFile.getAbsolutePath());
		fwkConfig.put("eclipse.consoleLog", "true");

		Equinox equinox = new Equinox(fwkConfig);
		equinox.init();
		try {
			final List<LogEntry> logEntries = new ArrayList<>();
			LogReaderService logReaderService = getLogReaderService(equinox);
			LogListener logListener = new SynchronousLogListener() {
				@Override
				public void logged(LogEntry entry) {
					logEntries.add(entry);
				}
			};
			logReaderService.addLogListener(logListener);
			Map<String, Location> locations = getLocations(equinox);
			Location userLocation = locations.get(Location.USER_FILTER);
			Location instanceLocation = locations.get(Location.INSTANCE_FILTER);
			assertNotNull("User locatoin is not null.", userLocation.getURL());
			assertNotNull("Instance location is not null.", instanceLocation.getURL());
			assertEquals("Wrong number of log entries", 2, logEntries.size());
		} finally {
			equinox.stop();
		}
	}

	private LogReaderService getLogReaderService(Equinox equinox) {
		return equinox.getBundleContext().getService(equinox.getBundleContext().getServiceReference(LogReaderService.class));
	}
}
