/*
 * Copyright 2016 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.config.annotation;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationPolicy;
import static org.springframework.data.gemfire.config.annotation.EnableExpiration.ExpirationType;

import java.util.concurrent.atomic.AtomicReference;

import org.apache.geode.cache.AttributesMutator;
import org.apache.geode.cache.CustomExpiry;
import org.apache.geode.cache.ExpirationAction;
import org.apache.geode.cache.ExpirationAttributes;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.junit.After;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.gemfire.expiration.ExpirationActionType;
import org.springframework.data.gemfire.util.ArrayUtils;

/**
 * Unit tests for the {@link EnableExpiration} annotation and {@link ExpirationConfiguration} class.
 *
 * @author John Blum
 * @see org.junit.Test
 * @see org.mockito.Mockito
 * @see org.springframework.context.ConfigurableApplicationContext
 * @see org.springframework.data.gemfire.config.annotation.EnableExpiration
 * @see org.springframework.data.gemfire.config.annotation.ExpirationConfiguration
 * @see org.apache.geode.cache.CustomExpiry
 * @see org.apache.geode.cache.ExpirationAttributes
 * @see org.apache.geode.cache.Region
 * @since 1.9.0
 */
public class EnableExpirationConfigurationUnitTests {

	private ConfigurableApplicationContext applicationContext;

	@After
	public void tearDown() {
		if (applicationContext != null) {
			applicationContext.close();
		}
	}

	protected <K, V> void assertRegionExpiration(ExpirationAttributes expectedExpirationAttributes,
			Region<K, V> region, V... applicationDomainObjects) {

		assertIdleTimeoutExpiration(expectedExpirationAttributes, region, applicationDomainObjects);
		assertTimeToLiveExpiration(expectedExpirationAttributes, region, applicationDomainObjects);
	}

	protected <K, V> void assertIdleTimeoutExpiration(ExpirationAttributes expectedExpirationAttributes,
			Region<K, V> region, V... applicationDomainObjects) {

		assertExpiration(expectedExpirationAttributes, region.getAttributes().getCustomEntryIdleTimeout(),
			applicationDomainObjects);
	}

	protected <K, V> void assertNoIdleTimeoutExpiration(Region<K, V> region) {
		assertThat(region.getAttributes().getCustomEntryIdleTimeout()).isNull();
	}

	protected <K, V> void assertTimeToLiveExpiration(ExpirationAttributes expectedExpirationAttributes,
			Region<K, V> region, V... applicationDomainObjects) {

		assertExpiration(expectedExpirationAttributes, region.getAttributes().getCustomEntryTimeToLive(),
			applicationDomainObjects);
	}

	protected <K, V> void assertNoTimeToLiveExpiration(Region<K, V> region) {
		assertThat(region.getAttributes().getCustomEntryTimeToLive()).isNull();
	}

	@SuppressWarnings("unchecked")
	protected <K, V> void assertExpiration(ExpirationAttributes expectedExpirationAttributes,
			CustomExpiry<K, V> customExpiry, V... applicationDomainObjects) {

		Region.Entry<K, V> regionEntry = mockRegionEntry(ArrayUtils.getFirst(applicationDomainObjects));

		assertExpiration(customExpiry.getExpiry(regionEntry), expectedExpirationAttributes);
	}

	@SuppressWarnings({ "unchecked", "unused" })
	protected <K, V> void assertExpiration(ExpirationAttributes actualExpirationAttributes,
			ExpirationAttributes expectedExpirationAttributes) {

		assertThat(actualExpirationAttributes).isEqualTo(expectedExpirationAttributes);
	}

	@SuppressWarnings("unchecked")
	protected <K, V> Region<K, V> getRegion(String beanName) {
		return applicationContext.getBean(beanName, Region.class);
	}

	protected AnnotationConfigApplicationContext newApplicationContext(Class<?>... annotatedClasses) {
		return new AnnotationConfigApplicationContext(annotatedClasses);
	}

	protected ExpirationAttributes newExpirationAttributes(int timeout, ExpirationActionType action) {
		return newExpirationAttributes(timeout, action.getExpirationAction());
	}

	protected ExpirationAttributes newExpirationAttributes(int timeout, ExpirationAction action) {
		return new ExpirationAttributes(timeout, action);
	}

	@Test
	public void usesDefaultExpirationPolicyConfiguration() {
		applicationContext = newApplicationContext(DefaultExpirationPolicyConfiguration.class);

		ExpirationAttributes expectedExpiration = newExpirationAttributes(0, ExpirationActionType.INVALIDATE);

		Region one = getRegion("One");
		Region two = getRegion("Two");

		assertIdleTimeoutExpiration(expectedExpiration, one);
		assertIdleTimeoutExpiration(expectedExpiration, two);

		assertNoTimeToLiveExpiration(one);
		assertNoTimeToLiveExpiration(two);
	}

	@Test
	public void usesCustomExpirationPolicyConfiguration() {
		applicationContext = newApplicationContext(CustomExpirationPolicyConfiguration.class);

		ExpirationAttributes expectedExpiration = newExpirationAttributes(300, ExpirationActionType.LOCAL_DESTROY);

		Region one = getRegion("One");
		Region two = getRegion("Two");

		assertIdleTimeoutExpiration(expectedExpiration, one);
		assertIdleTimeoutExpiration(expectedExpiration, two);

		assertNoTimeToLiveExpiration(one);
		assertNoTimeToLiveExpiration(two);
	}

	@Test
	public void usesRegionSpecificExpirationPolicyConfiguration() {
		applicationContext = newApplicationContext(RegionSpecificExpirationPolicyConfiguration.class);

		ExpirationAttributes expectedIdleTimeout = newExpirationAttributes(180, ExpirationActionType.INVALIDATE);
		ExpirationAttributes expectedTimeToLive = newExpirationAttributes(360, ExpirationActionType.DESTROY);

		Region one = getRegion("One");
		Region two = getRegion("Two");

		assertIdleTimeoutExpiration(expectedIdleTimeout, one);
		assertNoIdleTimeoutExpiration(two);

		assertNoTimeToLiveExpiration(one);
		assertTimeToLiveExpiration(expectedTimeToLive, two);
	}

	@Test
	public void usesMixedExpirationPolicyConfiguration() {
		applicationContext = newApplicationContext(MixedExpirationPolicyConfiguration.class);

		ExpirationAttributes expectedIdleTimeout = newExpirationAttributes(120, ExpirationActionType.LOCAL_INVALIDATE);
		ExpirationAttributes expectedTimeToLive = newExpirationAttributes(600, ExpirationActionType.DESTROY);

		Region one = getRegion("One");
		Region two = getRegion("Two");

		assertIdleTimeoutExpiration(expectedIdleTimeout, one);
		assertNoIdleTimeoutExpiration(two);

		assertTimeToLiveExpiration(expectedTimeToLive, one);
		assertTimeToLiveExpiration(expectedTimeToLive, two);
	}

	@SuppressWarnings("unchecked")
	static <K, V> Region<K, V> mockRegion(String name) {
		Region<K, V> mockRegion = mock(Region.class);

		when(mockRegion.getName()).thenReturn(name);
		when(mockRegion.getFullPath()).thenReturn(String.format("%1$s%2$s", Region.SEPARATOR, name));

		final AtomicReference<CustomExpiry<K, V>> entryIdleTimeout = new AtomicReference<CustomExpiry<K, V>>(null);
		final AtomicReference<CustomExpiry<K, V>> entryTimeToLive = new AtomicReference<CustomExpiry<K, V>>(null);

		AttributesMutator<K, V> mockAttributesMutator = mock(AttributesMutator.class);

		when(mockRegion.getAttributesMutator()).thenReturn(mockAttributesMutator);

		doAnswer(new Answer<CustomExpiry<K, V>>() {
			@Override
			public CustomExpiry<K, V> answer(InvocationOnMock invocation) throws Throwable {
				CustomExpiry<K, V> customExpiry = invocation.getArgument(0);
				entryIdleTimeout.set(customExpiry);
				return customExpiry;
			}
		}).when(mockAttributesMutator).setCustomEntryIdleTimeout(any(CustomExpiry.class));

		doAnswer(new Answer<CustomExpiry<K, V>>() {
			@Override
			public CustomExpiry<K, V> answer(InvocationOnMock invocation) throws Throwable {
				CustomExpiry<K, V> customExpiry = invocation.getArgument(0);
				entryTimeToLive.set(customExpiry);
				return customExpiry;
			}
		}).when(mockAttributesMutator).setCustomEntryTimeToLive(any(CustomExpiry.class));

		RegionAttributes<K, V> mockRegionAttributes = mock(RegionAttributes.class);

		when(mockRegion.getAttributes()).thenReturn(mockRegionAttributes);

		when(mockRegionAttributes.getCustomEntryIdleTimeout()).thenAnswer(new Answer<CustomExpiry<K, V>>() {
			@Override public CustomExpiry<K, V> answer(InvocationOnMock invocation) throws Throwable {
				return entryIdleTimeout.get();
			}
		});

		when(mockRegionAttributes.getCustomEntryTimeToLive()).thenAnswer(new Answer<CustomExpiry<K, V>>() {
			@Override public CustomExpiry<K, V> answer(InvocationOnMock invocation) throws Throwable {
				return entryTimeToLive.get();
			}
		});

		return mockRegion;
	}

	@SuppressWarnings("unchecked")
	static <K, V> Region.Entry<K, V> mockRegionEntry(V applicationDomainObject) {
		Region.Entry<K, V> mockRegionEntry = mock(Region.Entry.class);
		when(mockRegionEntry.getValue()).thenReturn(applicationDomainObject);
		return mockRegionEntry;
	}

	@Configuration
	@SuppressWarnings("unused")
	static class RegionConfiguration {

		@Bean("One")
		Region<Object, Object> regionOne() {
			return mockRegion("One");
		}

		@Bean("Two")
		Region<Object, Object> regionTwo() {
			return mockRegion("Two");
		}
	}

	@EnableExpiration
	static class DefaultExpirationPolicyConfiguration extends RegionConfiguration {
	}

	@EnableExpiration(policies = { @ExpirationPolicy(timeout = 300, action = ExpirationActionType.LOCAL_DESTROY) })
	static class CustomExpirationPolicyConfiguration extends RegionConfiguration {
	}

	@EnableExpiration(policies = {
		@ExpirationPolicy(timeout = 180, action = ExpirationActionType.INVALIDATE, regionNames = "One"),
		@ExpirationPolicy(timeout = 360, action = ExpirationActionType.DESTROY, regionNames = "Two", types = ExpirationType.TIME_TO_LIVE)
	})
	static class RegionSpecificExpirationPolicyConfiguration extends RegionConfiguration {
	}

	@EnableExpiration(policies = {
		@ExpirationPolicy(timeout = 120, action = ExpirationActionType.LOCAL_INVALIDATE, regionNames = "One", types = ExpirationType.IDLE_TIMEOUT),
		@ExpirationPolicy(timeout = 600, action = ExpirationActionType.DESTROY, types = ExpirationType.TIME_TO_LIVE)
	})
	static class MixedExpirationPolicyConfiguration extends RegionConfiguration {
	}
}
