/*******************************************************************************
 * Copyright (c) 2020 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:
 *     Alex Blewitt - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.common.tests;

import static java.util.Collections.singletonMap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.osgi.framework.FrameworkUtil.asDictionary;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.core.runtime.ServiceCaller;
import org.junit.Test;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceRegistration;

public class ServiceCallerTest {
	static class ServiceExampleFactory implements ServiceFactory<IServiceExample> {
		final Map<Bundle, AtomicInteger> createCount = new ConcurrentHashMap<>();
		volatile ServiceExample lastCreated;

		@Override
		public IServiceExample getService(Bundle bundle, ServiceRegistration<IServiceExample> registration) {
			createCount.computeIfAbsent(bundle, (s) -> new AtomicInteger()).incrementAndGet();
			return lastCreated = new ServiceExample();
		}

		@Override
		public void ungetService(Bundle bundle, ServiceRegistration<IServiceExample> registration,
				IServiceExample service) {
			// do nothing
		}

		int getCreateCount(Bundle b) {
			AtomicInteger result = createCount.get(b);
			return result == null ? 0 : result.get();
		}
	}

	@Test
	public void testCallOnce() {
		Bundle bundle = FrameworkUtil.getBundle(ServiceCallerTest.class);
		assertNotNull("Test only works under an OSGi runtime", bundle);
		BundleContext context = bundle.getBundleContext();
		ServiceExampleFactory factory = new ServiceExampleFactory();
		ServiceRegistration<IServiceExample> reg = null;
		try {
			Dictionary<String, String> props = new Hashtable<>();
			props.put("test", "value");

			reg = context.registerService(IServiceExample.class, factory, props);
			ServiceCaller.callOnce(IServiceExample.class, IServiceExample.class, IServiceExample::call);
			ServiceExample lastCreated1 = factory.lastCreated;
			assertTrue("Service called successfully", lastCreated1.called);

			Bundle[] users = reg.getReference().getUsingBundles();
			assertNull("Didn't expect users.", users);

			ServiceCaller.callOnce(IServiceExample.class, IServiceExample.class, "(test=value)", IServiceExample::call);
			ServiceExample lastCreated2 = factory.lastCreated;
			assertTrue("Service called successfully", lastCreated2.called);

			assertNotEquals("Should have new service each call", lastCreated1, lastCreated2);

			boolean result = ServiceCaller.callOnce(IServiceExample.class, IServiceExample.class, "(test!=value)",
					IServiceExample::call);
			assertFalse("Should not have found service with filter", result);

			assertEquals("Unexpected createCount", 2, factory.getCreateCount(bundle));
		} finally {
			reg.unregister();
		}
	}

	@Test
	public void testCall() throws IOException, BundleException, ClassNotFoundException {
		Bundle bundle = FrameworkUtil.getBundle(ServiceCallerTest.class);
		assertNotNull("Test only works under an OSGi runtime", bundle);
		BundleContext context = bundle.getBundleContext();

		Bundle testBundle = StatusTest.installTestClassBundle(context);
		testBundle.start();

		ServiceCaller<IServiceExample> thisClassCaller;
		ServiceCaller<IServiceExample> thisClassCallerFilter;
		ServiceCaller<IServiceExample> otherClassCaller;

		thisClassCaller = new ServiceCaller<>(getClass(), IServiceExample.class);
		thisClassCallerFilter = new ServiceCaller<>(getClass(), IServiceExample.class, "(test=value2)");
		Class<?> testClass = testBundle.loadClass(TestClass.class.getName());
		otherClassCaller = new ServiceCaller<>(testClass, IServiceExample.class);

		assertFalse("Should not be called.", thisClassCaller.call(IServiceExample::call));
		assertFalse("Should not be called.", thisClassCallerFilter.call(IServiceExample::call));
		assertFalse("Should not be called.", otherClassCaller.call(IServiceExample::call));

		assertFalse("Should not be present.", thisClassCaller.current().isPresent());
		assertFalse("Should not be present.", thisClassCallerFilter.current().isPresent());
		assertFalse("Should not be present.", otherClassCaller.current().isPresent());

		ServiceExampleFactory factory = new ServiceExampleFactory();
		Dictionary<String, String> props = new Hashtable<>();
		props.put("test", "value1");
		ServiceRegistration<IServiceExample> reg = context.registerService(IServiceExample.class, factory, props);
		try {
			assertTrue("Call returned false.", thisClassCaller.call(IServiceExample::call));
			assertTrue("Service called successfully", factory.lastCreated.called);
			assertTrue("Should be present.", thisClassCaller.current().isPresent());
			assertEquals("Unexpected current.", thisClassCaller.current().get(), factory.lastCreated);

			assertFalse("Should not be called.", thisClassCallerFilter.call(IServiceExample::call));

			factory.lastCreated.called = false;
			assertTrue("Call returned false.", otherClassCaller.call(IServiceExample::call));
			assertTrue("Service called successfully", factory.lastCreated.called);

			assertTrue("Call returned false.", thisClassCaller.call(IServiceExample::call));
			assertTrue("Call returned false.", otherClassCaller.call(IServiceExample::call));

			assertEquals("Wrong createCount", 1, factory.getCreateCount(bundle));
			assertEquals("Wrong createCount", 1, factory.getCreateCount(testBundle));

			Bundle[] users = reg.getReference().getUsingBundles();
			assertNotNull("Expected some users", users);

			Collection<Bundle> userCollection = Arrays.asList(users);
			assertTrue("Missing bundle.", userCollection.contains(bundle));
			assertTrue("Missing bundle.", userCollection.contains(testBundle));

			reg.unregister();
			assertFalse("Should not be called.", thisClassCaller.call(IServiceExample::call));
			assertFalse("Should not be called.", otherClassCaller.call(IServiceExample::call));
			assertFalse("Should not be present.", thisClassCaller.current().isPresent());

			props.put("test", "value2");
			reg = context.registerService(IServiceExample.class, factory, props);

			assertTrue("Should be present.", thisClassCaller.current().isPresent());
			assertEquals("Unexpected current.", thisClassCaller.current().get(), factory.lastCreated);
			assertTrue("Call returned false.", thisClassCaller.call(IServiceExample::call));
			assertTrue("Call returned false.", thisClassCallerFilter.call(IServiceExample::call));
			assertTrue("Call returned false.", otherClassCaller.call(IServiceExample::call));
			assertTrue("Call returned false.", thisClassCaller.call(IServiceExample::call));
			assertTrue("Call returned false.", thisClassCallerFilter.call(IServiceExample::call));
			assertTrue("Call returned false.", otherClassCaller.call(IServiceExample::call));
			assertEquals("Wrong createCount", 2, factory.getCreateCount(bundle));
			assertEquals("Wrong createCount", 2, factory.getCreateCount(testBundle));

			testBundle.stop();
			assertFalse("Should Not be called.", otherClassCaller.call(IServiceExample::call));

			testBundle.start();
			assertTrue("Call returned false.", otherClassCaller.call(IServiceExample::call));
			assertEquals("Wrong createCount", 3, factory.getCreateCount(testBundle));

			thisClassCaller.unget();
			thisClassCallerFilter.unget();
			assertTrue("Call returned false.", thisClassCaller.call(IServiceExample::call));
			assertTrue("Call returned false.", thisClassCallerFilter.call(IServiceExample::call));
			assertEquals("Wrong createCount", 3, factory.getCreateCount(bundle));

			props.put("test", "value3");
			reg.setProperties(props);
			assertTrue("Call returned false.", thisClassCaller.call(IServiceExample::call));
			assertFalse("Should not be called.", thisClassCallerFilter.call(IServiceExample::call));
			assertFalse("Should not be present.", thisClassCallerFilter.current().isPresent());
		} finally {
			testBundle.uninstall();
			thisClassCaller.unget();
			otherClassCaller.unget();
			try {
				reg.unregister();
			} catch (IllegalStateException e) {
				// ignore
			}
		}
	}

	@Test
	public void testInvalidFilter() {
		IllegalArgumentException exceptionOnConstructor = assertThrows(IllegalArgumentException.class,
				() -> new ServiceCaller<>(getClass(), IServiceExample.class, "invalid filter"));
		assertThat("Unexpected cause.", exceptionOnConstructor.getCause(), instanceOf(InvalidSyntaxException.class));

		IllegalArgumentException exceptionOnCall = assertThrows(IllegalArgumentException.class, () -> {
			ServiceCaller.callOnce(getClass(), IServiceExample.class, "invalid filter", (example) -> {
			});
		});
		assertThat("Unexpected cause.", exceptionOnCall.getCause(), instanceOf(InvalidSyntaxException.class));
	}

	@Test
	public void testRank() {
		Bundle bundle = FrameworkUtil.getBundle(ServiceCallerTest.class);
		assertNotNull("Test only works under an OSGi runtime", bundle);
		BundleContext context = bundle.getBundleContext();
		ServiceExample s1 = new ServiceExample();
		ServiceExample s2 = new ServiceExample();
		ServiceExample s3 = new ServiceExample();
		ServiceRegistration<IServiceExample> reg1 = null;
		ServiceRegistration<IServiceExample> reg2 = null;
		ServiceRegistration<IServiceExample> reg3 = null;
		try {
			reg1 = context.registerService(IServiceExample.class, s1,
					asDictionary(singletonMap(Constants.SERVICE_RANKING, 1)));
			reg2 = context.registerService(IServiceExample.class, s2,
					asDictionary(singletonMap(Constants.SERVICE_RANKING, 2)));
			reg3 = context.registerService(IServiceExample.class, s3,
					asDictionary(singletonMap(Constants.SERVICE_RANKING, 3)));
			ServiceCaller<IServiceExample> caller = new ServiceCaller<>(getClass(), IServiceExample.class);

			assertHighestRankedCalled(caller, s3, s2, s1, reg3, reg2, reg1);

			reg1.setProperties(asDictionary(singletonMap(Constants.SERVICE_RANKING, 10)));
			assertHighestRankedCalled(caller, s1, s2, s3, reg1, reg2, reg3);

			reg2.setProperties(asDictionary(singletonMap(Constants.SERVICE_RANKING, 10)));
			reg1.setProperties(asDictionary(singletonMap(Constants.SERVICE_RANKING, 11)));
			assertHighestRankedCalled(caller, s1, s2, s3, reg1, reg2, reg3);

			reg1.setProperties(asDictionary(singletonMap(Constants.SERVICE_RANKING, 1)));
			assertHighestRankedCalled(caller, s2, s1, s3, reg2, reg1, reg3);

			// re-register a new service with highest rank
			reg3.unregister();
			reg3 = context.registerService(IServiceExample.class, s3,
					asDictionary(singletonMap(Constants.SERVICE_RANKING, 200)));
			assertHighestRankedCalled(caller, s3, s2, s1, reg3, reg2, reg1);

			// re-register another service with lower rank
			reg1.unregister();
			reg1 = context.registerService(IServiceExample.class, s1,
					asDictionary(singletonMap(Constants.SERVICE_RANKING, 200)));
			assertHighestRankedCalled(caller, s3, s2, s1, reg3, reg2, reg1);
		} finally {
			if (reg1 != null) {
				reg1.unregister();
			}
			if (reg2 != null) {
				reg2.unregister();
			}
			if (reg3 != null) {
				reg3.unregister();
			}
		}
	}

	private void assertHighestRankedCalled(ServiceCaller<IServiceExample> caller, ServiceExample highest,
			ServiceExample lower1, ServiceExample lower2, ServiceRegistration<IServiceExample> highestReg,
			ServiceRegistration<IServiceExample> lower1Reg, ServiceRegistration<IServiceExample> lower2Reg) {
		assertTrue("Did not call service.", caller.call(IServiceExample::call));
		assertTrue("Highest Ranked not called.", highest.called);
		assertFalse("Lower1 called.", lower1.called);
		assertFalse("Lower2 called.", lower2.called);
		assertNotNull("No users of highest.", highestReg.getReference().getUsingBundles());
		assertNull("Users of lower ranked.", lower1Reg.getReference().getUsingBundles());
		assertNull("Users of lower ranked.", lower2Reg.getReference().getUsingBundles());
		highest.called = false;
		lower1.called = false;
		lower2.called = false;
	}

	interface IServiceExample {
		void call();
	}

	static class ServiceExample implements IServiceExample {
		boolean called = false;

		@Override
		public void call() {
			called = true;
		}

	}
}
