/*
 * 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.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import javax.annotation.Resource;

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.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.gemfire.test.GemfireTestApplicationContextInitializer;
import org.springframework.expression.EvaluationException;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * The AnnotationBasedExpirationConfigurationIntegrationTest class is a test suite of test cases testing
 * the configuration of Annotation-defined expiration policies on Region Entry TTL and TTI
 * custom expiration settings.
 *
 * @author John Blum
 * @see org.junit.Test
 * @see org.junit.runner.RunWith
 * @see org.mockito.Mockito
 * @see AnnotationBasedExpiration
 * @see org.springframework.data.gemfire.test.GemfireTestApplicationContextInitializer
 * @see org.springframework.test.context.ContextConfiguration
 * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner
 * @see org.apache.geode.cache.CustomExpiry
 * @see org.apache.geode.cache.ExpirationAction
 * @see org.apache.geode.cache.ExpirationAttributes
 * @since 1.7.0
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(initializers = GemfireTestApplicationContextInitializer.class)
@SuppressWarnings("unused")
public class AnnotationBasedExpirationConfigurationIntegrationTest {

	@Rule
	public ExpectedException exception = ExpectedException.none();

	@Autowired
	@Qualifier("genericExpiration")
	private AnnotationBasedExpiration<Object, Object> genericExpiration;

	@Autowired
	@Qualifier("ttiExpiration")
	private AnnotationBasedExpiration<Object, Object> idleTimeoutExpiration;

	@Autowired
	@Qualifier("ttlExpiration")
	private AnnotationBasedExpiration<Object, Object> timeToLiveExpiration;

	@Autowired
	private ExpirationAttributes defaultExpirationAttributes;

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

	@Before
	public void setup() {
		assertThat(example, is(not(nullValue())));
		assertThat(example.getName(), is(equalTo("Example")));
		assertThat(example.getFullPath(), is(equalTo(String.format("%1$s%2$s", Region.SEPARATOR, "Example"))));
		assertThat(example.getAttributes(), is(not(nullValue())));
		assertThat(defaultExpirationAttributes, is(not(nullValue())));
		assertThat(defaultExpirationAttributes.getTimeout(), is(equalTo(600)));
		assertThat(defaultExpirationAttributes.getAction(), is(equalTo(ExpirationAction.DESTROY)));
		assertThat(genericExpiration, is(instanceOf(CustomExpiry.class)));
		assertThat(genericExpiration.getDefaultExpirationAttributes(), is(nullValue()));
		assertThat(idleTimeoutExpiration, is(instanceOf(CustomExpiry.class)));
		assertThat(idleTimeoutExpiration.getDefaultExpirationAttributes(), is(nullValue()));
		assertThat(timeToLiveExpiration, is(instanceOf(CustomExpiry.class)));
		assertThat(timeToLiveExpiration.getDefaultExpirationAttributes(), is(sameInstance(defaultExpirationAttributes)));
	}

	private void assertExpiration(ExpirationAttributes expected, ExpirationAttributes actual) {
		assertExpiration(actual, expected.getTimeout(), expected.getAction());
	}

	private 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)));
	}

	@SuppressWarnings("unchecked")
	private Region.Entry<Object, Object> mockRegionEntry(Object value) {

		Region.Entry<Object, Object> mockRegionEntry = mock(Region.Entry.class, "MockRegionEntry");

		when(mockRegionEntry.getValue()).thenReturn(value);

		return mockRegionEntry;
	}

	@Test
	public void exampleRegionIdleTimeoutExpirationPolicy() {

		CustomExpiry<Object, Object> expiration = example.getAttributes().getCustomEntryIdleTimeout();

		assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithTimeToLiveAndGenericExpirationPolicies())),
			60, ExpirationAction.INVALIDATE);
		assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithIdleTimeoutExpirationPolicy())),
			120, ExpirationAction.INVALIDATE);
		assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithGenericExpirationPolicy())),
			60, ExpirationAction.DESTROY);
		assertThat(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithNoExpirationPolicy())), is(nullValue()));
		assertThat(expiration.getExpiry(mockRegionEntry(new RegionEntryTimeToLiveExpirationPolicy())), is(nullValue()));
		assertExpiration(expiration.getExpiry(mockRegionEntry(new RegionEntryIdleTimeoutExpirationPolicy())),
			60, ExpirationAction.INVALIDATE);
		assertExpiration(expiration.getExpiry(mockRegionEntry(new RegionEntryGenericExpirationPolicy())),
			60, ExpirationAction.DESTROY);
	}

	@Test
	public void exampleRegionTimeToLiveExpirationPolicy() {

		CustomExpiry<Object, Object> expiration = example.getAttributes().getCustomEntryTimeToLive();

		assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithTimeToLiveAndGenericExpirationPolicies())),
			300, ExpirationAction.DESTROY);
		assertExpiration(defaultExpirationAttributes, expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithIdleTimeoutExpirationPolicy())));
		assertExpiration(expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithGenericExpirationPolicy())),
			60, ExpirationAction.DESTROY);
		assertExpiration(defaultExpirationAttributes, expiration.getExpiry(mockRegionEntry(new ApplicationDomainObjectWithNoExpirationPolicy())));
		assertExpiration(expiration.getExpiry(mockRegionEntry(new RegionEntryTimeToLiveExpirationPolicy())),
			60, ExpirationAction.LOCAL_INVALIDATE);
		assertExpiration(defaultExpirationAttributes, expiration.getExpiry(mockRegionEntry(new RegionEntryIdleTimeoutExpirationPolicy())));
		assertExpiration(expiration.getExpiry(mockRegionEntry(new RegionEntryGenericExpirationPolicy())),
			60, ExpirationAction.DESTROY);
	}

	@Test
	public void genericExpirationPolicy() {

		assertExpiration(genericExpiration.getExpiry(mockRegionEntry(
			new ApplicationDomainObjectWithTimeToLiveAndGenericExpirationPolicies())), 60, ExpirationAction.INVALIDATE);
		assertThat(genericExpiration.getExpiry(mockRegionEntry(
			new ApplicationDomainObjectWithIdleTimeoutExpirationPolicy())), is(nullValue()));
		assertExpiration(genericExpiration.getExpiry(mockRegionEntry(
			new ApplicationDomainObjectWithGenericExpirationPolicy())), 60, ExpirationAction.DESTROY);
		assertThat(genericExpiration.getExpiry(mockRegionEntry(
			new ApplicationDomainObjectWithNoExpirationPolicy())), is(nullValue()));
		assertThat(genericExpiration.getExpiry(mockRegionEntry(
			new RegionEntryTimeToLiveExpirationPolicy())), is(nullValue()));
		assertThat(genericExpiration.getExpiry(mockRegionEntry(
			new RegionEntryIdleTimeoutExpirationPolicy())), is(nullValue()));
		assertExpiration(genericExpiration.getExpiry(mockRegionEntry(
			new RegionEntryGenericExpirationPolicy())), 60, ExpirationAction.DESTROY);
	}

	@Test
	public void invalidExpirationAction() {

		exception.expect(EvaluationException.class);
		exception.expectCause(isA(IllegalArgumentException.class));
		exception.expectMessage(String.format("[%s] is not resolvable as an ExpirationAction(Type)",
			"@expirationProperties['gemfire.region.entry.expiration.invalid.action.string']"));

		genericExpiration.getExpiry(mockRegionEntry(new RegionEntryWithInvalidExpirationAction()));
	}

	@Test
	public void invalidExpirationTimeout() {

		exception.expect(IllegalArgumentException.class);
		exception.expectCause(is(nullValue(Throwable.class)));

		genericExpiration.getExpiry(mockRegionEntry(new RegionEntryWithInvalidExpirationTimeout()));
	}

	@Expiration(timeout = "60", action = "INVALIDATE")
	@TimeToLiveExpiration(timeout = "300", action = "DESTROY")
	protected static class ApplicationDomainObjectWithTimeToLiveAndGenericExpirationPolicies { }

	@IdleTimeoutExpiration(timeout = "120", action = "INVALIDATE")
	protected static class ApplicationDomainObjectWithIdleTimeoutExpirationPolicy { }

	@Expiration(timeout = "60", action = "DESTROY")
	protected static class ApplicationDomainObjectWithGenericExpirationPolicy { }

	protected static class ApplicationDomainObjectWithNoExpirationPolicy { }

	@TimeToLiveExpiration(timeout = "${gemfire.region.entry.expiration.timeout}",
		action = "${gemfire.region.entry.expiration.action.string}")
	protected static class RegionEntryTimeToLiveExpirationPolicy { }

	@IdleTimeoutExpiration(timeout = "@expirationProperties['gemfire.region.entry.expiration.timeout']",
		action = "@expirationProperties['gemfire.region.entry.expiration.action.gemfire.type']")
	protected static class RegionEntryIdleTimeoutExpirationPolicy { }

	@Expiration(timeout = "${gemfire.region.entry.expiration.timeout}",
		action = "@expirationProperties['gemfire.region.entry.expiration.action.spring.type']")
	protected static class RegionEntryGenericExpirationPolicy { }

	@Expiration(timeout = "${gemfire.region.entry.expiration.timeout}",
		action = "@expirationProperties['gemfire.region.entry.expiration.invalid.action.string']")
	protected static class RegionEntryWithInvalidExpirationAction { }

	@Expiration(timeout = "${gemfire.region.entry.expiration.invalid.timeout}",
		action = "@expirationProperties['gemfire.region.entry.expiration.action.spring.type']")
	protected static class RegionEntryWithInvalidExpirationTimeout { }

}
