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

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.data.gemfire.config.annotation.EnableEviction.EvictionPolicy;

import java.util.concurrent.atomic.AtomicReference;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.EvictionAttributes;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.util.ObjectSizer;
import org.junit.After;
import org.junit.Test;
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.PartitionedRegionFactoryBean;
import org.springframework.data.gemfire.ReplicatedRegionFactoryBean;
import org.springframework.data.gemfire.eviction.EvictionActionType;
import org.springframework.data.gemfire.eviction.EvictionAttributesFactoryBean;
import org.springframework.data.gemfire.eviction.EvictionPolicyType;
import org.springframework.data.gemfire.util.ArrayUtils;

/**
 * Unit tests for the {@link EnableEviction} annotation and {@link EvictionConfiguration} class.
 *
 * @author John Blum
 * @see org.junit.Test
 * @see org.mockito.Mockito
 * @see org.springframework.data.gemfire.config.annotation.EnableEviction
 * @see org.springframework.data.gemfire.config.annotation.EvictionConfiguration
 * @see EvictionAttributesFactoryBean
 * @see org.apache.geode.cache.Region
 * @see org.apache.geode.cache.EvictionAttributes
 * @since 1.9.0
 */
public class EnableEvictionConfigurationUnitTests {

	private ConfigurableApplicationContext applicationContext;

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

	protected void assertEvictionAttributes(Region region, EvictionAttributes expectedEvictionAttributes) {
		assertThat(region).isNotNull();
		assertThat(region.getAttributes()).isNotNull();
		assertEvictionAttributes(region.getAttributes().getEvictionAttributes(), expectedEvictionAttributes);
	}

	protected void assertEvictionAttributes(EvictionAttributes actualEvictionAttributes,
		EvictionAttributes expectedEvictionAttributes) {

		assertThat(actualEvictionAttributes).isNotNull();
		assertThat(actualEvictionAttributes.getAction()).isEqualTo(expectedEvictionAttributes.getAction());
		assertThat(actualEvictionAttributes.getAlgorithm()).isEqualTo(expectedEvictionAttributes.getAlgorithm());
		assertThat(actualEvictionAttributes.getObjectSizer()).isEqualTo(expectedEvictionAttributes.getObjectSizer());

		if (!EvictionPolicyType.HEAP_PERCENTAGE.equals(
				EvictionPolicyType.valueOf(actualEvictionAttributes.getAlgorithm()))) {
			assertThat(actualEvictionAttributes.getMaximum()).isEqualTo(expectedEvictionAttributes.getMaximum());
		}
	}

	@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 EvictionAttributes newEvictionAttributes(Integer maximum, EvictionPolicyType type, EvictionActionType action,
			ObjectSizer... objectSizer) {

		EvictionAttributesFactoryBean evictionAttributesFactory = new EvictionAttributesFactoryBean();

		evictionAttributesFactory.setAction(action.getEvictionAction());
		evictionAttributesFactory.setObjectSizer(ArrayUtils.getFirst(objectSizer));
		evictionAttributesFactory.setThreshold(maximum);
		evictionAttributesFactory.setType(type);
		evictionAttributesFactory.afterPropertiesSet();

		return evictionAttributesFactory.getObject();
	}

	@Test
	public void usesDefaultEvictionPolicyConfiguration() {
		applicationContext = newApplicationContext(DefaultEvictionPolicyConfiguration.class);

		EvictionAttributes defaultEvictionAttributes = EvictionAttributes.createLRUEntryAttributes();

		assertEvictionAttributes(applicationContext.getBean("PartitionRegion", Region.class), defaultEvictionAttributes);
		assertEvictionAttributes(applicationContext.getBean("ReplicateRegion", Region.class), defaultEvictionAttributes);
	}

	@Test
	public void usesCustomEvictionPolicyConfiguration() {
		applicationContext = newApplicationContext(CustomEvictionPolicyConfiguration.class);

		ObjectSizer mockObjectSizer = applicationContext.getBean("mockObjectSizer", ObjectSizer.class);

		EvictionAttributes customEvictionAttributes = newEvictionAttributes(65536, EvictionPolicyType.MEMORY_SIZE,
			EvictionActionType.OVERFLOW_TO_DISK, mockObjectSizer);

		assertEvictionAttributes(applicationContext.getBean("PartitionRegion", Region.class), customEvictionAttributes);
		assertEvictionAttributes(applicationContext.getBean("ReplicateRegion", Region.class), customEvictionAttributes);
	}

	@Test
	public void usesRegionSpecificEvictionPolicyConfiguration() {
		applicationContext = newApplicationContext(RegionSpecificEvictionPolicyConfiguration.class);

		ObjectSizer mockObjectSizer = applicationContext.getBean("mockObjectSizer", ObjectSizer.class);

		EvictionAttributes partitionRegionEvictionAttributes = newEvictionAttributes(null,
			EvictionPolicyType.HEAP_PERCENTAGE, EvictionActionType.OVERFLOW_TO_DISK, mockObjectSizer);

		EvictionAttributes replicateRegionEvictionAttributes = newEvictionAttributes(10000,
			EvictionPolicyType.ENTRY_COUNT, EvictionActionType.LOCAL_DESTROY);

		assertEvictionAttributes(applicationContext.getBean("PartitionRegion", Region.class),
			partitionRegionEvictionAttributes);

		assertEvictionAttributes(applicationContext.getBean("ReplicateRegion", Region.class),
			replicateRegionEvictionAttributes);
	}

	@Test
	public void usesLastMatchingEvictionPolicyConfiguration() {
		applicationContext = newApplicationContext(LastMatchingWinsEvictionPolicyConfiguration.class);

		EvictionAttributes lastMatchingEvictionAttributes = newEvictionAttributes(99, EvictionPolicyType.ENTRY_COUNT,
			EvictionActionType.OVERFLOW_TO_DISK);

		assertEvictionAttributes(applicationContext.getBean("PartitionRegion", Region.class),
			lastMatchingEvictionAttributes);

		assertEvictionAttributes(applicationContext.getBean("ReplicateRegion", Region.class),
			lastMatchingEvictionAttributes);
	}

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

		@Bean("mockCache")
		@SuppressWarnings("unchecked")
		Cache mockCache() {

			Cache mockCache = mock(Cache.class);

			RegionFactory mockRegionFactory = mock(RegionFactory.class);

			AtomicReference<EvictionAttributes> evictionAttributes = new AtomicReference<>(null);

			when(mockCache.createRegionFactory()).thenReturn(mockRegionFactory);

			when(mockRegionFactory.setEvictionAttributes(any(EvictionAttributes.class)))
				.thenAnswer((Answer<RegionFactory>) invocation -> {
					evictionAttributes.set(invocation.getArgument(0));
					return (RegionFactory) invocation.getMock();
				}
			);

			when(mockRegionFactory.create(anyString()))
				.thenAnswer((Answer<Region>) invocation -> {

					String regionName = invocation.getArgument(0);

					Region mockRegion = mock(Region.class, regionName);

					RegionAttributes mockRegionAttributes =
						mock(RegionAttributes.class, regionName.concat("Attributes"));

					when(mockRegion.getName()).thenReturn(regionName);
					when(mockRegion.getFullPath()).thenReturn(String.format("%1$s%2$s", Region.SEPARATOR, regionName));
					when(mockRegion.getAttributes()).thenReturn(mockRegionAttributes);
					when(mockRegionAttributes.getEvictionAttributes()).thenReturn(evictionAttributes.get());

					return mockRegion;
				});

			return mockCache;
		}

		@Bean("PartitionRegion")
		PartitionedRegionFactoryBean<Object, Object> mockPartitionRegion(Cache gemfireCache) {

			PartitionedRegionFactoryBean<Object, Object> partitionRegion =
				new PartitionedRegionFactoryBean<>();

			partitionRegion.setCache(gemfireCache);
			partitionRegion.setClose(false);
			partitionRegion.setPersistent(false);

			return partitionRegion;
		}

		@Bean("ReplicateRegion")
		ReplicatedRegionFactoryBean<Object, Object> mockReplicateRegion(Cache gemfireCache) {

			ReplicatedRegionFactoryBean<Object, Object> replicateRegion =
				new ReplicatedRegionFactoryBean<>();

			replicateRegion.setCache(gemfireCache);
			replicateRegion.setClose(false);
			replicateRegion.setPersistent(false);

			return replicateRegion;
		}

		@Bean
		ObjectSizer mockObjectSizer() {
			return mock(ObjectSizer.class);
		}
	}

	@EnableEviction
	static class DefaultEvictionPolicyConfiguration extends CacheRegionConfiguration { }

	@EnableEviction(policies = @EvictionPolicy(maximum = 65536, type = EvictionPolicyType.MEMORY_SIZE,
		action = EvictionActionType.OVERFLOW_TO_DISK, objectSizerName = "mockObjectSizer"))
	static class CustomEvictionPolicyConfiguration extends CacheRegionConfiguration { }

	@EnableEviction(policies = {
		@EvictionPolicy(maximum = 85, type = EvictionPolicyType.HEAP_PERCENTAGE, action = EvictionActionType.OVERFLOW_TO_DISK,
			objectSizerName = "mockObjectSizer", regionNames = "PartitionRegion"),
		@EvictionPolicy(maximum = 10000, type = EvictionPolicyType.ENTRY_COUNT, action = EvictionActionType.LOCAL_DESTROY,
			regionNames = "ReplicateRegion")
	})
	static class RegionSpecificEvictionPolicyConfiguration extends CacheRegionConfiguration { }

	@EnableEviction(policies = {
		@EvictionPolicy(maximum = 1, type = EvictionPolicyType.ENTRY_COUNT, action = EvictionActionType.LOCAL_DESTROY,
			objectSizerName = "mockObjectSizer", regionNames = "ReplicateRegion"),
		@EvictionPolicy(maximum = 99, type = EvictionPolicyType.ENTRY_COUNT, action = EvictionActionType.OVERFLOW_TO_DISK)
	})
	static class LastMatchingWinsEvictionPolicyConfiguration extends CacheRegionConfiguration { }

}
