/*
 * Copyright 2010-2013 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.data.gemfire.client;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assume.assumeTrue;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Resource;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.gemfire.GemfireUtils;
import org.springframework.data.gemfire.process.ProcessWrapper;
import org.springframework.data.gemfire.test.AbstractGemFireClientServerIntegrationTest;
import org.springframework.data.gemfire.test.support.ThreadUtils;
import org.springframework.data.gemfire.util.DistributedSystemUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.Assert;
import org.springframework.util.SocketUtils;

import com.gemstone.gemfire.cache.DataPolicy;
import com.gemstone.gemfire.cache.EntryEvent;
import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.cache.client.ClientCache;
import com.gemstone.gemfire.cache.client.ClientCacheFactory;
import com.gemstone.gemfire.cache.client.ClientRegionShortcut;
import com.gemstone.gemfire.cache.client.Pool;
import com.gemstone.gemfire.cache.util.CacheListenerAdapter;

/**
 * The DurableClientCacheIntegrationTest class is a test suite of test cases testing GemFire's Durable Client
 * functionality in the context of Spring Data GemFire.
 *
 * @author John Blum
 * @see org.junit.Test
 * @see org.junit.runner.RunWith
 * @see org.springframework.beans.factory.config.BeanPostProcessor
 * @see org.springframework.context.ConfigurableApplicationContext
 * @see org.springframework.data.gemfire.process.ProcessWrapper
 * @see org.springframework.data.gemfire.test.AbstractGemFireClientServerIntegrationTest
 * @see org.springframework.test.context.ContextConfiguration
 * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner
 * @see com.gemstone.gemfire.cache.client.ClientCache
 * @see com.gemstone.gemfire.cache.Region
 * @see com.gemstone.gemfire.cache.util.CacheListenerAdapter
 * @since 1.6.3
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@SuppressWarnings("all")
public class DurableClientCacheIntegrationTest extends AbstractGemFireClientServerIntegrationTest {

	private static int serverPort;

	private static AtomicBoolean DIRTIES_CONTEXT = new AtomicBoolean(false);

	private static List<Integer> regionCacheListenerEventValues =
		Collections.synchronizedList(new ArrayList<Integer>());

	private static ProcessWrapper serverProcess;

	private static final String CACHE_SERVER_PORT_SYSTEM_PROPERTY =
		DurableClientCacheIntegrationTest.class.getName().concat(".cache-server-port");

	private static final String CLIENT_CACHE_INTERESTS_RESULT_POLICY_SYSTEM_PROPERTY =
		DurableClientCacheIntegrationTest.class.getName().concat(".interests-result-policy");

	private static final String DURABLE_CLIENT_TIMEOUT_SYSTEM_PROPERTY =
		DurableClientCacheIntegrationTest.class.getName().concat(".durable-client-timeout");

	private static final String SERVER_HOST = "localhost";

	@Autowired
	private ConfigurableApplicationContext applicationContext;

	@Autowired
	private ClientCache clientCache;

	@Resource(name = "Example")
	private Region<String, Integer> example;

	@BeforeClass
	public static void setupGemFireServer() throws IOException {
		serverPort = setSystemProperty(CACHE_SERVER_PORT_SYSTEM_PROPERTY, SocketUtils.findAvailableTcpPort());
		serverProcess = startGemFireServer(DurableClientCacheIntegrationTest.class);
	}

	@AfterClass
	public static void tearDownGemFireServer() {
		serverProcess = stopGemFireServer(serverProcess);
		clearSystemProperties(DurableClientCacheIntegrationTest.class);
	}

	protected static boolean isAfterDirtiesContext() {
		return DIRTIES_CONTEXT.get();
	}

	protected static boolean isBeforeDirtiesContext() {
		return !isAfterDirtiesContext();
	}

	protected boolean dirtiesContext() {
		return !DIRTIES_CONTEXT.getAndSet(true);
	}

	protected <T> T valueBeforeAndAfterDirtiesContext(T before, T after) {
		return (isBeforeDirtiesContext() ? before : after);
	}

	@Before
	public void setup() {
		Properties distributedSystemProperties = clientCache.getDistributedSystem().getProperties();

		assertThat(distributedSystemProperties.getProperty(
			DistributedSystemUtils.DURABLE_CLIENT_ID_PROPERTY_NAME),
				is(equalTo(DurableClientCacheIntegrationTest.class.getSimpleName())));

		assertThat(clientCache.getDistributedSystem().getProperties().getProperty(
			DistributedSystemUtils.DURABLE_CLIENT_TIMEOUT_PROPERTY_NAME),
				is(equalTo(valueBeforeAndAfterDirtiesContext("300", "600"))));

		assertRegion(example, "Example", DataPolicy.NORMAL);
	}

	@After
	public void tearDown() {
		if (dirtiesContext()) {
			closeApplicationContext();
			runClientCacheProducer();
			setSystemProperties();
		}

		regionCacheListenerEventValues.clear();
	}

	protected void closeApplicationContext() {
		applicationContext.close();

		assertThat(applicationContext.isRunning(), is(false));
		assertThat(applicationContext.isActive(), is(false));
	}

	protected void runClientCacheProducer() {
		try {
			ClientCache gemfireClientCache = new ClientCacheFactory()
				.addPoolServer(SERVER_HOST, serverPort)
				.set("name", "ClientCacheProducer")
				.set("mcast-port", "0")
				.set("log-level", "warning")
				.create();

			Region<String, Integer> exampleRegion = gemfireClientCache.<String, Integer>createClientRegionFactory(
				ClientRegionShortcut.PROXY).create("Example");

			exampleRegion.put("four", 4);
			exampleRegion.put("five", 5);
		}
		finally {
			GemfireUtils.closeClientCache();
		}
	}

	protected void setSystemProperties() {
		System.setProperty(CLIENT_CACHE_INTERESTS_RESULT_POLICY_SYSTEM_PROPERTY, InterestResultPolicyType.NONE.name());
		System.setProperty(DURABLE_CLIENT_TIMEOUT_SYSTEM_PROPERTY, "600");
	}

	protected void assertRegion(Region<?, ?> region, String expectedName, DataPolicy expectedDataPolicy) {
		assertRegion(region, expectedName, String.format("%1$s%2$s", Region.SEPARATOR, expectedName),
			expectedDataPolicy);
	}

	protected void assertRegion(Region<?, ?> region, String expectedName, String expectedPath,
			DataPolicy expectedDataPolicy) {

		assertThat(region, is(notNullValue()));
		assertThat(region.getName(), is(equalTo(expectedName)));
		assertThat(region.getFullPath(), is(equalTo(expectedPath)));
		assertThat(region.getAttributes(), is(notNullValue()));
		assertThat(region.getAttributes().getDataPolicy(), is(equalTo(expectedDataPolicy)));
	}

	protected void assertRegionValues(Region<?, ?> region, Object... values) {
		assertThat(region.size(), is(equalTo(values.length)));

		for (Object value : values) {
			assertThat(region.containsValue(value), is(true));
		}
	}

	protected void waitForRegionEntryEvents() {
		ThreadUtils.timedWait(TimeUnit.SECONDS.toMillis(5), TimeUnit.MILLISECONDS.toMillis(500),
			new ThreadUtils.WaitCondition() {
				@Override public boolean waiting() {
					return (regionCacheListenerEventValues.size() < 2);
				}
			}
		);
	}

	@Test
	@DirtiesContext
	public void durableClientGetsInitializedWithDataOnServer() {
		assumeTrue(isBeforeDirtiesContext());
		assertRegionValues(example, 1, 2, 3);
		assertThat(regionCacheListenerEventValues.isEmpty(), is(true));
	}

	@Test
	public void durableClientGetsUpdatesFromServerWhileClientWasOffline() {
		assumeTrue(isAfterDirtiesContext());
		assertThat(example.isEmpty(), is(true));

		waitForRegionEntryEvents();

		assertThat(regionCacheListenerEventValues.size(), is(equalTo(2)));
		assertThat(regionCacheListenerEventValues, is(equalTo(Arrays.asList(4, 5))));
		assertThat(example.isEmpty(), is(true));
	}

	public static class ClientCacheBeanPostProcessor implements BeanPostProcessor {

		@Override
		public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
			return bean;
		}

		@Override
		public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
			if (bean instanceof Pool && "gemfireServerPool".equals(beanName)) {
				Pool gemfireServerPool = (Pool) bean;

				if (isBeforeDirtiesContext()) {
					// NOTE: A value of -2 indicates the client connected to the server for the first time...
					assertThat(gemfireServerPool.getPendingEventCount(), is(equalTo(-2)));
				}
				else {
					// NOTE: the pending event count could be 3 because it should minimally include the 2 puts
					// from the client cache producer and possibly a "marker" as well...
					assertThat(gemfireServerPool.getPendingEventCount(), is(greaterThanOrEqualTo(2)));
				}
			}

			return bean;
		}
	}

	public static class RegionDataLoadingBeanPostProcessor<K, V> implements BeanPostProcessor {

		private Map<K, V> regionData;

		private final String regionName;

		public RegionDataLoadingBeanPostProcessor(String regionName) {
			Assert.hasText(regionName, "Region name must be specified");
			this.regionName = regionName;
		}

		public void setRegionData(Map<K, V> regionData) {
			this.regionData = regionData;
		}

		protected Map<K, V> getRegionData() {
			Assert.state(regionData != null, "Region data was not properly initialized");
			return regionData;
		}

		protected String getRegionName() {
			return regionName;
		}

		protected void loadData(Region<K, V> region) {
			region.putAll(getRegionData());
		}

		@Override
		public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
			return bean;
		}

		@Override
		public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
			if (bean instanceof Region) {
				Region<K, V> region = (Region) bean;

				if (getRegionName().equals(region.getName())) {
					loadData(region);
				}
			}

			return bean;
		}
	}

	public static class RegionEntryEventRecordingCacheListener extends CacheListenerAdapter<String, Integer> {

		@Override
		public void afterCreate(EntryEvent<String, Integer> event) {
			regionCacheListenerEventValues.add(event.getNewValue());
		}
	}
}
