/*
 * Copyright 2002-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.integration.redis.outbound;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.mock;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.junit.Test;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.support.collections.DefaultRedisList;
import org.springframework.data.redis.support.collections.DefaultRedisZSet;
import org.springframework.data.redis.support.collections.RedisCollectionFactoryBean.CollectionType;
import org.springframework.data.redis.support.collections.RedisList;
import org.springframework.data.redis.support.collections.RedisZSet;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.redis.rules.RedisAvailable;
import org.springframework.integration.redis.rules.RedisAvailableTests;
import org.springframework.integration.redis.support.RedisHeaders;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.support.GenericMessage;

/**
 * @author Oleg Zhurakousky
 * @author Gunnar Hillert
 * @author Mark Fisher
 * @author Gary Russell
 * @author Artem Bilan
 */
public class RedisStoreWritingMessageHandlerTests extends RedisAvailableTests {

	@Test
	@RedisAvailable
	public void testListWithListPayloadParsedAndProvidedKey() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deleteKey(jcf, "foo");
		String key = "foo";
		RedisList<String> redisList =
				new DefaultRedisList<String>(key, this.initTemplate(jcf, new StringRedisTemplate()));

		assertThat(redisList.size()).isEqualTo(0);

		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setKey(key);
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		List<String> list = new ArrayList<String>();
		list.add("Manny");
		list.add("Moe");
		list.add("Jack");
		Message<List<String>> message = new GenericMessage<List<String>>(list);
		handler.handleMessage(message);

		assertThat(redisList.size()).isEqualTo(3);
		assertThat(redisList.get(0)).isEqualTo("Manny");
		assertThat(redisList.get(1)).isEqualTo("Moe");
		assertThat(redisList.get(2)).isEqualTo("Jack");
		this.deleteKey(jcf, "foo");
	}

	@Test
	@RedisAvailable
	public void testListWithListPayloadParsedAndProvidedKeyAsHeader() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deleteKey(jcf, "foo");
		String key = "foo";
		RedisList<String> redisList =
				new DefaultRedisList<String>(key, this.initTemplate(jcf, new StringRedisTemplate()));

		assertThat(redisList.size()).isEqualTo(0);

		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);

		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		List<String> list = new ArrayList<String>();
		list.add("Manny");
		list.add("Moe");
		list.add("Jack");
		Message<List<String>> message = MessageBuilder.withPayload(list).setHeader("redis_key", key).build();
		handler.handleMessage(message);

		assertThat(redisList.size()).isEqualTo(3);
		assertThat(redisList.get(0)).isEqualTo("Manny");
		assertThat(redisList.get(1)).isEqualTo("Moe");
		assertThat(redisList.get(2)).isEqualTo("Jack");
		this.deleteKey(jcf, "foo");
	}

	@RedisAvailable
	@Test(expected = MessageHandlingException.class)
	public void testListWithListPayloadParsedAndNoKey() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deleteKey(jcf, "foo");
		String key = "foo";
		RedisList<String> redisList =
				new DefaultRedisList<String>(key, this.initTemplate(jcf, new RedisTemplate<String, String>()));

		assertThat(redisList.size()).isEqualTo(0);

		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		List<String> list = new ArrayList<String>();
		list.add("Manny");
		list.add("Moe");
		list.add("Jack");
		Message<List<String>> message = MessageBuilder.withPayload(list).build();
		handler.handleMessage(message);
		this.deleteKey(jcf, "foo");
	}

	@Test
	@RedisAvailable
	public void testListWithListPayloadAsSingleEntry() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deleteKey(jcf, "foo");
		String key = "foo";
		RedisList<List<String>> redisList =
				new DefaultRedisList<List<String>>(key, this.initTemplate(jcf, new RedisTemplate<String, List<String>>()));

		assertThat(redisList.size()).isEqualTo(0);

		RedisTemplate<String, List<String>> template = this.initTemplate(jcf, new RedisTemplate<String, List<String>>());
		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(template);
		handler.setKey(key);
		handler.setExtractPayloadElements(false);
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		List<String> list = new ArrayList<String>();
		list.add("Manny");
		list.add("Moe");
		list.add("Jack");
		Message<List<String>> message = new GenericMessage<List<String>>(list);
		handler.handleMessage(message);

		assertThat(redisList.size()).isEqualTo(1);
		List<String> resultList = redisList.get(0);
		assertThat(resultList.get(0)).isEqualTo("Manny");
		assertThat(resultList.get(1)).isEqualTo("Moe");
		assertThat(resultList.get(2)).isEqualTo("Jack");
		this.deleteKey(jcf, "foo");
	}

	@Test
	@RedisAvailable
	public void testZsetWithListPayloadParsedAndProvidedKeyDefault() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deleteKey(jcf, "foo");
		String key = "foo";
		RedisZSet<String> redisZset =
				new DefaultRedisZSet<String>(key, this.initTemplate(jcf, new StringRedisTemplate()));

		assertThat(redisZset.size()).isEqualTo(0);

		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setKey(key);
		handler.setCollectionType(CollectionType.ZSET);
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		List<String> list = new ArrayList<String>();
		list.add("Manny");
		list.add("Moe");
		list.add("Jack");
		Message<List<String>> message = MessageBuilder.withPayload(list)
				.setHeader(RedisHeaders.ZSET_INCREMENT_SCORE, true)
				.build();
		handler.handleMessage(message);

		assertThat(redisZset.size()).isEqualTo(3);
		Set<TypedTuple<String>> pepboys = redisZset.rangeByScoreWithScores(1, 1);
		for (TypedTuple<String> pepboy : pepboys) {
			assertThat(pepboy.getScore() == 1).isTrue();
		}

		handler.handleMessage(message);
		assertThat(redisZset.size()).isEqualTo(3);
		pepboys = redisZset.rangeByScoreWithScores(1, 2);
		// should have incremented by 1
		for (TypedTuple<String> pepboy : pepboys) {
			assertThat(pepboy.getScore()).isEqualTo(Double.valueOf(2));
		}
		this.deleteKey(jcf, "foo");
	}

	@Test
	@RedisAvailable
	public void testZsetWithListPayloadParsedAndProvidedKeyScoreIncrement() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deleteKey(jcf, "foo");
		String key = "foo";
		RedisZSet<String> redisZset =
				new DefaultRedisZSet<String>(key, this.initTemplate(jcf, new StringRedisTemplate()));

		assertThat(redisZset.size()).isEqualTo(0);

		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setKey(key);
		handler.setCollectionType(CollectionType.ZSET);
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		List<String> list = new ArrayList<String>();
		list.add("Manny");
		list.add("Moe");
		list.add("Jack");
		Message<List<String>> message = MessageBuilder.withPayload(list)
				.setHeader(RedisHeaders.ZSET_INCREMENT_SCORE, Boolean.TRUE)
				.build();

		handler.handleMessage(message);

		assertThat(redisZset.size()).isEqualTo(3);
		Set<TypedTuple<String>> pepboys = redisZset.rangeByScoreWithScores(1, 1);
		for (TypedTuple<String> pepboy : pepboys) {
			assertThat(pepboy.getScore() == 1).isTrue();
		}

		handler.handleMessage(message);
		assertThat(redisZset.size()).isEqualTo(3);
		pepboys = redisZset.rangeByScoreWithScores(1, 2);
		// should have incremented
		for (TypedTuple<String> pepboy : pepboys) {
			assertThat(pepboy.getScore() == 2).isTrue();
		}
		this.deleteKey(jcf, "foo");
	}

	@Test
	@RedisAvailable
	public void testZsetWithListPayloadParsedAndProvidedKeyScoreIncrementAsStringHeader() { // see INT-2775
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deleteKey(jcf, "foo");
		String key = "foo";
		RedisZSet<String> redisZset =
				new DefaultRedisZSet<String>(key, this.initTemplate(jcf, new StringRedisTemplate()));

		assertThat(redisZset.size()).isEqualTo(0);

		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setKey(key);
		handler.setCollectionType(CollectionType.ZSET);
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		List<String> list = new ArrayList<String>();
		list.add("Manny");
		list.add("Moe");
		list.add("Jack");
		Message<List<String>> message = MessageBuilder.withPayload(list)
				.setHeader(RedisHeaders.ZSET_INCREMENT_SCORE, "true")
				.build();

		handler.handleMessage(message);

		assertThat(redisZset.size()).isEqualTo(3);
		Set<TypedTuple<String>> pepboys = redisZset.rangeByScoreWithScores(1, 1);
		for (TypedTuple<String> pepboy : pepboys) {
			assertThat(pepboy.getScore() == 1).isTrue();
		}

		handler.handleMessage(message);
		assertThat(redisZset.size()).isEqualTo(3);
		pepboys = redisZset.rangeByScoreWithScores(1, 2);
		// should have incremented
		for (TypedTuple<String> pepboy : pepboys) {
			assertThat(pepboy.getScore() == 2).isTrue();
		}
		this.deleteKey(jcf, "foo");
	}

	@Test
	@RedisAvailable
	public void testZsetWithListPayloadAsSingleEntryAndHeaderKeyHeaderScore() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deleteKey(jcf, "foo");
		String key = "foo";
		RedisZSet<List<String>> redisZset =
				new DefaultRedisZSet<List<String>>(key, this.initTemplate(jcf, new RedisTemplate<String, List<String>>()));

		assertThat(redisZset.size()).isEqualTo(0);

		RedisTemplate<String, List<String>> template = this.initTemplate(jcf, new RedisTemplate<String, List<String>>());
		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(template);

		handler.setCollectionType(CollectionType.ZSET);
		handler.setExtractPayloadElements(false);
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		List<String> list = new ArrayList<String>();
		list.add("Manny");
		list.add("Moe");
		list.add("Jack");
		Message<List<String>> message = MessageBuilder.withPayload(list).setHeader("redis_key", key).
						setHeader("redis_zsetScore", 4).build();
		handler.handleMessage(message);

		assertThat(redisZset.size()).isEqualTo(1);
		Set<TypedTuple<List<String>>> entries = redisZset.rangeByScoreWithScores(1, 4);
		for (TypedTuple<List<String>> pepboys : entries) {
			assertThat(pepboys.getScore() == 4).isTrue();
		}
		this.deleteKey(jcf, "foo");
	}

	@Test
	@RedisAvailable
	public void testZsetWithMapPayloadParsedHeaderKey() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deletePresidents(jcf);
		String key = "presidents";
		RedisZSet<String> redisZset =
				new DefaultRedisZSet<String>(key, this.initTemplate(jcf, new StringRedisTemplate()));

		assertThat(redisZset.size()).isEqualTo(0);

		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setKey(key);
		handler.setCollectionType(CollectionType.ZSET);
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		Map<String, Double> presidents = new HashMap<String, Double>();
		presidents.put("John Adams", 18D);

		presidents.put("Barack Obama", 21D);
		presidents.put("Thomas Jefferson", 19D);
		presidents.put("John Quincy Adams", 19D);
		presidents.put("Zachary Taylor", 19D);

		presidents.put("Theodore Roosevelt", 20D);
		presidents.put("Woodrow Wilson", 20D);
		presidents.put("George W. Bush", 21D);
		presidents.put("Franklin D. Roosevelt", 20D);
		presidents.put("Ronald Reagan", 20D);
		presidents.put("William J. Clinton", 20D);
		presidents.put("Abraham Lincoln", 19D);
		presidents.put("George Washington", 18D);

		Message<Map<String, Double>> message = MessageBuilder.withPayload(presidents).setHeader("redis_key", key).build();
		handler.handleMessage(message);

		assertThat(redisZset.size()).isEqualTo(13);

		Set<TypedTuple<String>> entries = redisZset.rangeByScoreWithScores(18, 19);
		assertThat(entries.size()).isEqualTo(6);
		this.deletePresidents(jcf);
	}

	@Test
	@RedisAvailable
	public void testZsetWithMapPayloadPojoParsedHeaderKey() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deletePresidents(jcf);
		String key = "presidents";
		RedisZSet<President> redisZset =
				new DefaultRedisZSet<President>(key, this.initTemplate(jcf, new RedisTemplate<String, President>()));

		assertThat(redisZset.size()).isEqualTo(0);

		RedisTemplate<String, President> template = this.initTemplate(jcf, new RedisTemplate<String, President>());
		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(template);
		handler.setKey(key);
		handler.setCollectionType(CollectionType.ZSET);
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		Map<President, Double> presidents = new HashMap<President, Double>();
		presidents.put(new President("John Adams"), 18D);

		presidents.put(new President("Barack Obama"), 21D);
		presidents.put(new President("Thomas Jefferson"), 19D);
		presidents.put(new President("John Quincy Adams"), 19D);
		presidents.put(new President("Zachary Taylor"), 19D);

		presidents.put(new President("Theodore Roosevelt"), 20D);
		presidents.put(new President("Woodrow Wilson"), 20D);
		presidents.put(new President("George W. Bush"), 21D);
		presidents.put(new President("Franklin D. Roosevelt"), 20D);
		presidents.put(new President("Ronald Reagan"), 20D);
		presidents.put(new President("William J. Clinton"), 20D);
		presidents.put(new President("Abraham Lincoln"), 19D);
		presidents.put(new President("George Washington"), 18D);

		Message<Map<President, Double>> message = MessageBuilder.withPayload(presidents).setHeader("redis_key", key).build();
		handler.handleMessage(message);

		assertThat(redisZset.size()).isEqualTo(13);

		Set<TypedTuple<President>> entries = redisZset.rangeByScoreWithScores(18, 19);
		assertThat(entries.size()).isEqualTo(6);
		this.deletePresidents(jcf);
	}

	@Test
	@RedisAvailable
	public void testZsetWithMapPayloadPojoAsSingleEntryHeaderKey() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deletePresidents(jcf);
		String key = "presidents";
		RedisZSet<Map<President, Double>> redisZset =
				new DefaultRedisZSet<Map<President, Double>>(key, this.initTemplate(jcf, new RedisTemplate<String, Map<President, Double>>()));

		assertThat(redisZset.size()).isEqualTo(0);

		RedisTemplate<String, Map<President, Double>> template = this.initTemplate(jcf, new RedisTemplate<String, Map<President, Double>>());
		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(template);
		handler.setKey(key);
		handler.setCollectionType(CollectionType.ZSET);
		handler.setExtractPayloadElements(false);
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();

		Map<President, Double> presidents = new HashMap<President, Double>();
		presidents.put(new President("John Adams"), 18D);

		presidents.put(new President("Barack Obama"), 21D);
		presidents.put(new President("Thomas Jefferson"), 19D);

		Message<Map<President, Double>> message = MessageBuilder.withPayload(presidents).setHeader("redis_key", key).build();
		handler.handleMessage(message);

		assertThat(redisZset.size()).isEqualTo(1);
		this.deletePresidents(jcf);
	}

	@Test(expected = IllegalStateException.class)
	@RedisAvailable
	public void testListWithMapKeyExpression() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		String key = "foo";
		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setKey(key);
		handler.setMapKeyExpression(new LiteralExpression(key));
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();
	}

	@Test(expected = IllegalStateException.class)
	@RedisAvailable
	public void testSetWithMapKeyExpression() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		String key = "foo";
		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setKey(key);
		handler.setCollectionType(CollectionType.SET);
		handler.setMapKeyExpression(new LiteralExpression(key));
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();
	}

	@Test(expected = IllegalStateException.class)
	@RedisAvailable
	public void testZsetWithMapKeyExpression() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		String key = "foo";
		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setKey(key);
		handler.setCollectionType(CollectionType.ZSET);
		handler.setMapKeyExpression(new LiteralExpression(key));
		handler.setBeanFactory(mock(BeanFactory.class));
		handler.afterPropertiesSet();
	}

	@Test
	@RedisAvailable
	public void testMapWithMapKeyExpression() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deleteKey(jcf, "foo");
		String key = "foo";
		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setKey(key);
		handler.setCollectionType(CollectionType.MAP);
		handler.setMapKeyExpression(new LiteralExpression(key));
		handler.setBeanFactory(mock(BeanFactory.class));
		try {
			handler.afterPropertiesSet();
		}
		catch (Exception e) {
			fail("No exception expected:" + e.getMessage());
		}
		this.deleteKey(jcf, "foo");
	}

	@Test
	@RedisAvailable
	public void testPropertiesWithMapKeyExpression() {
		RedisConnectionFactory jcf = this.getConnectionFactoryForTest();
		this.deleteKey(jcf, "foo");
		String key = "foo";
		RedisStoreWritingMessageHandler handler =
				new RedisStoreWritingMessageHandler(jcf);
		handler.setKey(key);
		handler.setCollectionType(CollectionType.PROPERTIES);
		handler.setMapKeyExpression(new LiteralExpression(key));
		handler.setBeanFactory(mock(BeanFactory.class));
		try {
			handler.afterPropertiesSet();
		}
		catch (Exception e) {
			fail("No exception expected:" + e.getMessage());
		}
		this.deleteKey(jcf, "foo");
	}

	private <K, V> RedisTemplate<K, V> initTemplate(RedisConnectionFactory rcf, RedisTemplate<K, V> redisTemplate) {
		redisTemplate.setConnectionFactory(rcf);
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.afterPropertiesSet();
		return redisTemplate;
	}

	private static class President implements Serializable {
		private static final long serialVersionUID = 1L;
		private String name;

		President(String name) {
			this.name = name;
		}

		@SuppressWarnings("unused")
		public String getName() {
			return name;
		}

		@SuppressWarnings("unused")
		public void setName(String name) {
			this.name = name;
		}
	}
}
