/*
 * Copyright 2016-2019 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
 *
 *      https://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.expiration;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.isA;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.data.gemfire.expiration.AnnotationBasedExpiration.ExpirationMetaData;

import org.apache.geode.cache.ExpirationAction;
import org.apache.geode.cache.ExpirationAttributes;
import org.apache.geode.cache.Region;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.gemfire.TestUtils;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.TypeLocator;
import org.springframework.expression.spel.support.StandardEvaluationContext;

/**
 * The AnnotationBasedExpirationTest class is a test suite of test cases testing the contract and functionality
 * of the AnnotationBasedExpirationTest.
 *
 * @author John Blum
 * @see org.junit.Test
 * @see org.mockito.Mockito
 * @see AnnotationBasedExpiration
 * @since 1.7.0
 */
@SuppressWarnings({ "rawtypes", "unchecked", "unused" })
public class AnnotationBasedExpirationTest {

	private AnnotationBasedExpiration noDefaultExpiration = new AnnotationBasedExpiration();

	protected void assertExpiration(ExpirationAttributes expirationAttributes, int expectedTimeout,
			ExpirationAction expectedAction) {

		assertThat(expirationAttributes, is(not(nullValue())));
		assertThat(expirationAttributes.getTimeout(), is(equalTo(expectedTimeout)));
		assertThat(expirationAttributes.getAction(), is(equalTo(expectedAction)));
	}

	protected void assertExpiration(ExpirationMetaData expirationMetaData, int expectedTimeout,
			ExpirationActionType expectedExpirationAction) {

		assertThat(expirationMetaData, is(not(nullValue())));
		assertThat(expirationMetaData.timeout(), is(equalTo(expectedTimeout)));
		assertThat(expirationMetaData.action(), is(equalTo(expectedExpirationAction)));
	}

	@Test
	public void constructUninitializedAnnotationBasedExpirationInstance() {
		AnnotationBasedExpiration expiration = new AnnotationBasedExpiration();

		assertThat(expiration.getDefaultExpirationAttributes(), is(nullValue()));
	}

	@Test
	public void constructInitializedAnnotationBasedExpirationInstance() {
		AnnotationBasedExpiration expiration = new AnnotationBasedExpiration(ExpirationAttributes.DEFAULT);

		assertThat(expiration.getDefaultExpirationAttributes(), is(equalTo(ExpirationAttributes.DEFAULT)));
	}

	@Test
	public void forIdleTimeoutNoDefaultExpiration() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		AnnotationBasedExpiration expiration = AnnotationBasedExpiration.forIdleTimeout();

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutExpiration());
		assertExpiration(expiration.getExpiry(mockRegionEntry), 120, ExpirationAction.LOCAL_INVALIDATE);
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveGenericExpiration());
		assertExpiration(expiration.getExpiry(mockRegionEntry), 60, ExpirationAction.INVALIDATE);
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithNoExpiration());
		assertThat(expiration.getExpiry(mockRegionEntry), is(nullValue()));
		verify(mockRegionEntry, atLeast(3)).getValue();
	}

	@Test
	public void forIdleTimeoutWithDefaultExpiration() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		ExpirationAttributes defaultExpiration = new ExpirationAttributes(300, ExpirationAction.DESTROY);

		AnnotationBasedExpiration expiration = AnnotationBasedExpiration.forIdleTimeout(defaultExpiration);

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutExpiration());
		assertExpiration(expiration.getExpiry(mockRegionEntry), 120, ExpirationAction.LOCAL_INVALIDATE);
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveGenericExpiration());
		assertExpiration(expiration.getExpiry(mockRegionEntry), 60, ExpirationAction.INVALIDATE);
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithNoExpiration());
		assertThat(expiration.getExpiry(mockRegionEntry), is(equalTo(defaultExpiration)));
		verify(mockRegionEntry, atLeast(3)).getValue();
	}

	@Test
	public void forTimeToLiveNoDefaultExpiration() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		AnnotationBasedExpiration expiration = AnnotationBasedExpiration.forTimeToLive();

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutExpiration());
		assertExpiration(expiration.getExpiry(mockRegionEntry), 300, ExpirationAction.LOCAL_DESTROY);
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithIdleTimeoutGenericExpiration());
		assertExpiration(expiration.getExpiry(mockRegionEntry), 60, ExpirationAction.INVALIDATE);
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithNoExpiration());
		assertThat(expiration.getExpiry(mockRegionEntry), is(nullValue()));
		verify(mockRegionEntry, atLeast(3)).getValue();
	}

	@Test
	public void forTimeToLiveWithDefaultExpiration() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		ExpirationAttributes defaultExpiration = new ExpirationAttributes(300, ExpirationAction.DESTROY);

		AnnotationBasedExpiration expiration = AnnotationBasedExpiration.forTimeToLive(defaultExpiration);

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutExpiration());
		assertExpiration(expiration.getExpiry(mockRegionEntry), 300, ExpirationAction.LOCAL_DESTROY);
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithIdleTimeoutGenericExpiration());
		assertExpiration(expiration.getExpiry(mockRegionEntry), 60, ExpirationAction.INVALIDATE);
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithNoExpiration());
		assertThat(expiration.getExpiry(mockRegionEntry), is(equalTo(defaultExpiration)));
		verify(mockRegionEntry, atLeast(3)).getValue();
	}

	@Test
	public void setAndGetBeanFactory() {
		final StandardEvaluationContext mockEvaluationContext = mock(StandardEvaluationContext.class,
			"MockStandardEvaluationContext");

		ConversionService mockConversionService = mock(ConversionService.class, "MockConversionService");

		final ConfigurableBeanFactory mockBeanFactory = mock(ConfigurableBeanFactory.class, "MockBeanFactory");

		when(mockBeanFactory.getConversionService()).thenReturn(mockConversionService);
		when(mockBeanFactory.getBeanClassLoader()).thenReturn(Thread.currentThread().getContextClassLoader());

		doAnswer(new Answer<Void>() {
			@Override public Void answer(final InvocationOnMock invocation) throws Throwable {
				BeanResolver beanResolver = invocation.getArgument(0);
				assertThat(beanResolver, is(instanceOf(BeanFactoryResolver.class)));
				assertThat(TestUtils.<ConfigurableBeanFactory>readField("beanFactory", beanResolver),
					is(equalTo(mockBeanFactory)));
				return null;
			}
		}).when(mockEvaluationContext).setBeanResolver(Matchers.any(BeanResolver.class));

		AnnotationBasedExpiration<Object, Object> annotationBasedExpiration = new AnnotationBasedExpiration<Object, Object>() {
			@Override StandardEvaluationContext newEvaluationContext() {
				return mockEvaluationContext;
			}
		};

		annotationBasedExpiration.setBeanFactory(mockBeanFactory);

		assertSame(mockBeanFactory, annotationBasedExpiration.getBeanFactory());

		verify(mockEvaluationContext, times(3)).addPropertyAccessor(Matchers.any(PropertyAccessor.class));
		verify(mockEvaluationContext, times(1)).setTypeConverter(Matchers.any(TypeConverter.class));
		verify(mockEvaluationContext, times(1)).setTypeLocator(Matchers.any(TypeLocator.class));
		verify(mockEvaluationContext, times(1)).setBeanResolver(Matchers.any(BeanResolver.class));
		verify(mockBeanFactory, times(1)).getConversionService();
		verify(mockBeanFactory, times(1)).getBeanClassLoader();
	}

	@Test(expected = IllegalStateException.class)
	public void getUninitializedBeanFactory() {
		new AnnotationBasedExpiration<Object, Object>().getBeanFactory();
	}

	@Test
	public void setAndGetDefaultExpirationAttributes() {
		ExpirationAttributes expectedExpirationAttributes = new ExpirationAttributes(120, ExpirationAction.INVALIDATE);
		AnnotationBasedExpiration expiration = new AnnotationBasedExpiration();

		expiration.setDefaultExpirationAttributes(expectedExpirationAttributes);

		assertThat(expiration.getDefaultExpirationAttributes(), is(equalTo(expectedExpirationAttributes)));

		expiration.setDefaultExpirationAttributes(null);

		assertThat(expiration.getDefaultExpirationAttributes(), is(nullValue()));

		expiration.setDefaultExpirationAttributes(ExpirationAttributes.DEFAULT);

		assertThat(expiration.getDefaultExpirationAttributes(), is(equalTo(ExpirationAttributes.DEFAULT)));
	}

	@Test
	public void getExpiryCallsGetExpirationMetaDataOnRegionEntryFollowedByNewExpirationAttributes() {
		final ExpirationAttributes expectedExpirationAttributes = new ExpirationAttributes(60,
			ExpirationAction.LOCAL_DESTROY);

		final Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		AnnotationBasedExpiration expiration = new AnnotationBasedExpiration() {
			@Override protected ExpirationMetaData getExpirationMetaData(Region.Entry entry) {
				assertThat(entry, is(sameInstance(mockRegionEntry)));
				return ExpirationMetaData.from(expectedExpirationAttributes);
			}

			@Override protected ExpirationAttributes newExpirationAttributes(ExpirationMetaData expirationMetaData) {
				assertThat(expirationMetaData.timeout(), is(equalTo(expectedExpirationAttributes.getTimeout())));
				assertThat(expirationMetaData.expirationAction(), is(equalTo(expectedExpirationAttributes.getAction())));
				return expectedExpirationAttributes;
			}
		};

		assertThat(expiration.getExpiry(mockRegionEntry), is(equalTo(expectedExpirationAttributes)));
	}

	@Test
	public void isExpirationConfiguredWithGenericExpirationBasedRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithGenericExpiration());
		assertThat(noDefaultExpiration.isExpirationConfigured(mockRegionEntry), is(true));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutGenericExpiration());
		assertThat(noDefaultExpiration.isExpirationConfigured(mockRegionEntry), is(true));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void isExpirationConfiguredWithNoGenericExpirationRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutExpiration());
		assertThat(noDefaultExpiration.isExpirationConfigured(mockRegionEntry), is(false));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithNoExpiration());
		assertThat(noDefaultExpiration.isExpirationConfigured(mockRegionEntry), is(false));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void isIdleTimeoutConfiguredWithIdleTimeoutExpirationBasedRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithIdleTimeoutExpiration());
		assertThat(noDefaultExpiration.isIdleTimeoutConfigured(mockRegionEntry), is(true));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutGenericExpiration());
		assertThat(noDefaultExpiration.isIdleTimeoutConfigured(mockRegionEntry), is(true));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void isIdleTimeoutConfiguredWithNoIdleTimeoutExpirationRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveGenericExpiration());
		assertThat(noDefaultExpiration.isIdleTimeoutConfigured(mockRegionEntry), is(false));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithNoExpiration());
		assertThat(noDefaultExpiration.isIdleTimeoutConfigured(mockRegionEntry), is(false));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void isTimeToLiveConfiguredWithTimeToLiveExpirationBasedRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveExpiration());
		assertThat(noDefaultExpiration.isTimeToLiveConfigured(mockRegionEntry), is(true));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutGenericExpiration());
		assertThat(noDefaultExpiration.isTimeToLiveConfigured(mockRegionEntry), is(true));
		verify(mockRegionEntry, times(2)).getValue();
	}

	public void isTimeToLiveConfiguredWithNoTimeToLiveExpirationRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithIdleTimeoutGenericExpiration());
		assertThat(noDefaultExpiration.isTimeToLiveConfigured(mockRegionEntry), is(false));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithNoExpiration());
		assertThat(noDefaultExpiration.isTimeToLiveConfigured(mockRegionEntry), is(false));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void getExpirationWithGenericExpirationBasedRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithGenericExpiration());
		assertThat(noDefaultExpiration.getExpiration(mockRegionEntry), isA(Expiration.class));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutGenericExpiration());
		assertThat(noDefaultExpiration.getExpiration(mockRegionEntry), isA(Expiration.class));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void getExpirationWithNoGenericExpirationRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutExpiration());
		assertThat(noDefaultExpiration.getExpiration(mockRegionEntry), is(nullValue()));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithNoExpiration());
		assertThat(noDefaultExpiration.getExpiration(mockRegionEntry), is(nullValue()));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void getIdleTimeoutWithIdleTimeoutExpirationBasedRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithIdleTimeoutExpiration());
		assertThat(noDefaultExpiration.getIdleTimeout(mockRegionEntry), isA(IdleTimeoutExpiration.class));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutGenericExpiration());
		assertThat(noDefaultExpiration.getIdleTimeout(mockRegionEntry), isA(IdleTimeoutExpiration.class));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void getIdleTimeoutWithNoIdleTimeoutExpirationRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveGenericExpiration());
		assertThat(noDefaultExpiration.getIdleTimeout(mockRegionEntry), is(nullValue()));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithNoExpiration());
		assertThat(noDefaultExpiration.getIdleTimeout(mockRegionEntry), is(nullValue()));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void getTimeToLiveWithTimeToLiveExpirationBasedRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveExpiration());
		assertThat(noDefaultExpiration.getTimeToLive(mockRegionEntry), isA(TimeToLiveExpiration.class));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithTimeToLiveIdleTimeoutGenericExpiration());
		assertThat(noDefaultExpiration.getTimeToLive(mockRegionEntry), isA(TimeToLiveExpiration.class));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void getTimeToLiveWithNoTimeToLiveExpirationRegionEntry() {
		Region.Entry mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithIdleTimeoutGenericExpiration());
		assertThat(noDefaultExpiration.getTimeToLive(mockRegionEntry), is(nullValue()));
		when(mockRegionEntry.getValue()).thenReturn(new RegionEntryValueWithNoExpiration());
		assertThat(noDefaultExpiration.getTimeToLive(mockRegionEntry), is(nullValue()));
		verify(mockRegionEntry, times(2)).getValue();
	}

	@Test
	public void fromExpiration() {
		ExpirationMetaData expirationMetaData = ExpirationMetaData.from(
			RegionEntryValueWithGenericExpiration.class.getAnnotation(Expiration.class));

		assertExpiration(expirationMetaData, 60, ExpirationActionType.INVALIDATE);
	}

	@Test
	public void fromExpirationIdleTimeout() {
		ExpirationMetaData expirationMetaData = ExpirationMetaData.from(
			RegionEntryValueWithIdleTimeoutExpiration.class.getAnnotation(IdleTimeoutExpiration.class));

		assertExpiration(expirationMetaData, 120, ExpirationActionType.LOCAL_INVALIDATE);
	}

	@Test
	public void fromExpirationTimeToLive() {
		ExpirationMetaData expirationMetaData = ExpirationMetaData.from(
			RegionEntryValueWithTimeToLiveExpiration.class.getAnnotation(TimeToLiveExpiration.class));

		assertExpiration(expirationMetaData, 300, ExpirationActionType.LOCAL_DESTROY);
	}

	@Test
	public void toExpirationAttributes() {
		ExpirationMetaData expirationMetaData = new ExpirationMetaData(90, ExpirationActionType.DESTROY);

		assertExpiration(expirationMetaData.toExpirationAttributes(), expirationMetaData.timeout(),
			expirationMetaData.expirationAction());
	}

	@Expiration(timeout = "60", action = "INVALIDATE")
	@IdleTimeoutExpiration(timeout = "120", action = "LOCAL_INVALIDATE")
	@TimeToLiveExpiration(timeout = "300", action = "LOCAL_DESTROY")
	public static class RegionEntryValueWithTimeToLiveIdleTimeoutGenericExpiration {
	}

	@IdleTimeoutExpiration(timeout = "120", action = "LOCAL_INVALIDATE")
	@TimeToLiveExpiration(timeout = "300", action = "LOCAL_DESTROY")
	public static class RegionEntryValueWithTimeToLiveIdleTimeoutExpiration {
	}

	@Expiration(timeout = "60", action = "INVALIDATE")
	@TimeToLiveExpiration(timeout = "300", action = "LOCAL_DESTROY")
	public static class RegionEntryValueWithTimeToLiveGenericExpiration {
	}

	@TimeToLiveExpiration(timeout = "300", action = "LOCAL_DESTROY")
	public static class RegionEntryValueWithTimeToLiveExpiration {
	}

	@Expiration(timeout = "60", action = "INVALIDATE")
	@IdleTimeoutExpiration(timeout = "120", action = "LOCAL_INVALIDATE")
	public static class RegionEntryValueWithIdleTimeoutGenericExpiration {
	}

	@IdleTimeoutExpiration(timeout = "120", action = "LOCAL_INVALIDATE")
	public static class RegionEntryValueWithIdleTimeoutExpiration {
	}

	@Expiration(timeout = "60", action = "INVALIDATE")
	public static class RegionEntryValueWithGenericExpiration {
	}

	public static class RegionEntryValueWithNoExpiration {
	}

}
