package appengine.util;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;

import static org.junit.Assert.assertThat;

/**
 * @author shin1ogawa
 */
public class DatastoreServiceTest {

	static final Logger logger = Logger.getLogger(DatastoreServiceTest.class.getName());


	/**
	 * {@link String}型の{@link List}を使った検索を試す。
	 */
	@Test
	public void listProperty_StringList() {
		Entity entity1 = _listProperty_StringList(new String[] {
			"aaa",
			"bbb",
			"ccc"
		});
		Key key1 = DatastoreServiceUtil.putWithRetry(entity1, 5);
		Entity entity2 = _listProperty_StringList(new String[] {
			"111",
			"222",
			"333"
		});
		Key key2 = DatastoreServiceUtil.putWithRetry(entity2, 5);
		Query query =
				new Query("kind").addFilter("list", FilterOperator.EQUAL, "111").addSort("list");
		DatastoreService service = DatastoreServiceFactory.getDatastoreService();
		List<Entity> list = service.prepare(query).asList(FetchOptions.Builder.withOffset(0));
		assertThat(list.size(), is(equalTo(1)));
		assertThat(list.get(0).getKey(), is(equalTo(key2)));
		query =
				new Query("kind").addFilter("list", FilterOperator.GREATER_THAN, "999").addSort(
						"list");
		list = service.prepare(query).asList(FetchOptions.Builder.withOffset(0));
		assertThat(list.size(), is(equalTo(1)));
		assertThat(list.get(0).getKey(), is(equalTo(key1)));
	}

	private Entity _listProperty_StringList(String[] stringArray) {
		Entity entity = new Entity("kind");
		List<String> strings = new ArrayList<String>();
		for (String string : stringArray) {
			strings.add(string);
		}
		entity.setProperty("list", strings);
		return entity;
	}

	/**
	 * {@link Date}型の{@link List}を使った検索を試す。
	 */
	@SuppressWarnings("deprecation")
	@Test
	public void listProperty_DateList() {
		Entity entity1 = _listProperty_DateList(new Date(2000, 1, 1), new Date(2100, 12, 31));
		Key key1 = DatastoreServiceUtil.putWithRetry(entity1, 5);
		Entity entity2 = _listProperty_DateList(new Date(2003, 1, 1), new Date(2003, 12, 31));
		Key key2 = DatastoreServiceUtil.putWithRetry(entity2, 5);
		Entity entity3 = _listProperty_DateList(new Date(2004, 1, 1), new Date(2050, 12, 31));
		Key key3 = DatastoreServiceUtil.putWithRetry(entity3, 5);

		Query query =
				new Query("kind").addFilter("list", FilterOperator.GREATER_THAN_OR_EQUAL,
						new Date(2000, 2, 1)).addFilter("list", FilterOperator.LESS_THAN_OR_EQUAL,
						new Date(2003, 12, 31)).addSort("list");
		List<Entity> list =
				DatastoreServiceFactory.getDatastoreService().prepare(query).asList(
						FetchOptions.Builder.withOffset(0));
		assertThat(list.size(), is(equalTo(2)));
		assertThat(list.get(0).getKey(), is(equalTo(key1)));
		assertThat(list.get(1).getKey(), is(equalTo(key2)));

		query =
				new Query("kind").addFilter("list", FilterOperator.GREATER_THAN_OR_EQUAL,
						new Date(2004, 2, 1)).addFilter("list", FilterOperator.LESS_THAN_OR_EQUAL,
						new Date(2010, 12, 31)).addSort("list");
		list =
				DatastoreServiceFactory.getDatastoreService().prepare(query).asList(
						FetchOptions.Builder.withOffset(0));
		assertThat(list.size(), is(equalTo(2)));
		assertThat(list.get(0).getKey(), is(equalTo(key1)));
		assertThat(list.get(1).getKey(), is(equalTo(key3)));
	}

	private Entity _listProperty_DateList(Date date1, Date date2) {
		Entity entity = new Entity("kind");
		List<Date> list = new ArrayList<Date>(2);
		list.add(date1);
		list.add(date2);
		entity.setProperty("list", list);
		return entity;
	}

	/**
	 * {@link String}型の{@link Set}を使った検索を試す。
	 * <p>これも検索可能だが、実は{@link ArrayList}に変換されて格納されている。</p>
	 */
	@Test
	public void listProperty_StringSet() {
		Entity entity1 = _listProperty_StringSet(new String[] {
			"aaa",
			"bbb",
			"ccc"
		});
		assertThat(entity1.getProperty("list"), is(instanceOf(HashSet.class)));
		Key key1 = DatastoreServiceUtil.putWithRetry(entity1, 5);
		Entity entity2 = _listProperty_StringSet(new String[] {
			"111",
			"222",
			"333"
		});
		Key key2 = DatastoreServiceUtil.putWithRetry(entity2, 5);
		Query query =
				new Query("kind").addFilter("list", FilterOperator.EQUAL, "111").addSort("list");
		DatastoreService service = DatastoreServiceFactory.getDatastoreService();
		List<Entity> list = service.prepare(query).asList(FetchOptions.Builder.withOffset(0));
		assertThat(list.size(), is(equalTo(1)));
		assertThat(list.get(0).getKey(), is(equalTo(key2)));
		query =
				new Query("kind").addFilter("list", FilterOperator.GREATER_THAN, "999").addSort(
						"list");
		list = service.prepare(query).asList(FetchOptions.Builder.withOffset(0));
		assertThat(list.size(), is(equalTo(1)));
		assertThat(list.get(0).getKey(), is(equalTo(key1)));
		// HashSetで保存したはずなのに、ArrayListになってる。
		assertThat(list.get(0).getProperty("list"), is(instanceOf(ArrayList.class)));
	}

	private Entity _listProperty_StringSet(String[] stringArray) {
		Entity entity = new Entity("kind");
		Set<String> strings = new HashSet<String>();
		for (String string : stringArray) {
			strings.add(string);
		}
		entity.setProperty("list", strings);
		return entity;
	}

	/**
	 * 親キーのみ指定したクエリでKindを跨いだEntityを取得できるか？
	 */
	@Test
	public void ancestorKeyQuery() {
		DatastoreService service = DatastoreServiceFactory.getDatastoreService();
		Iterator<Key> parentKeys = service.allocateIds("kind1", 2).iterator();

		Key parent1Key = parentKeys.next();
		Key parent2Key = parentKeys.next();

		Entity parent1 = new Entity(parent1Key);
		Key child1Key = service.allocateIds(parent1Key, "kind2", 1).getStart();
		Entity child1 = new Entity(child1Key);
		child1.setProperty("name", "kind1(1)/kind2(1)");
		Key childChild1Key = service.allocateIds(child1Key, "kind3", 1).getStart();
		Entity childChild1 = new Entity(childChild1Key);
		childChild1.setProperty("name", "kind1(1)/kind2(1)/kind3(1)");
		DatastoreTransactionUtil.runInTransaction(new DatastoreTransactionUtil.PutAllInTransaction(
				Arrays.asList(parent1, child1, childChild1)));

		Entity parent2 = new Entity(parent2Key);
		Key child2Key = service.allocateIds(parent2Key, "kind2", 1).getStart();
		Entity child2 = new Entity(child2Key);
		child2.setProperty("name", "kind1(2)/kind2(2)");
		Key childChild2Key = service.allocateIds(child2Key, "kind3", 1).getStart();
		Entity childChild2 = new Entity(childChild2Key);
		childChild2.setProperty("name", "kind1(2)/kind2(2)/kind3(2)");
		DatastoreTransactionUtil.runInTransaction(new DatastoreTransactionUtil.PutAllInTransaction(
				Arrays.asList(parent2, child2, childChild2)));

		List<Entity> list1 =
				service.prepare(new Query(parent1Key)).asList(FetchOptions.Builder.withOffset(0));
		if (AppEngineUtil.isDeployment()) {
			for (Entity entity : list1) {
				logger.log(Level.FINE, "entity key.id=" + entity.getKey().getId());
				logger.log(Level.FINE, ToStringBuilder.reflectionToString(entity));
			}
			// SDK1.2.5現在、親KeyのみのQueryはローカル環境で動作しない。
			assertThat(list1.size(), is(equalTo(3)));
			assertThat(list1.get(0).getKind(), is(equalTo("kind1")));
			assertThat(list1.get(1).getKind(), is(equalTo("kind2")));
			assertThat(list1.get(2).getKind(), is(equalTo("kind3")));
		}
		List<Entity> list2 =
				service.prepare(new Query(parent2Key)).asList(FetchOptions.Builder.withOffset(0));
		if (AppEngineUtil.isDeployment()) {
			for (Entity entity : list2) {
				logger.log(Level.FINE, "entity key.id=" + entity.getKey().getId());
				logger.log(Level.FINE, ToStringBuilder.reflectionToString(entity));
			}
			// SDK1.2.5現在、親KeyのみのQueryはローカル環境で動作しない。
			assertThat(list2.size(), is(equalTo(3)));
			assertThat(list2.get(0).getKind(), is(equalTo("kind1")));
			assertThat(list2.get(1).getKind(), is(equalTo("kind2")));
			assertThat(list2.get(2).getKind(), is(equalTo("kind3")));
		}
	}

//	/**
//	 * 複数のTransactionをbeginした時の挙動を確認する。
//	 */
//	@Test
//	public void 複数のTransactionを開く() {
//		DatastoreService service = DatastoreServiceFactory.getDatastoreService();
//		Transaction tx1 = service.beginTransaction();
//		Transaction tx2 = service.beginTransaction();
//	}

	/**
	 * ローカルでの実行時はファイルへの保存なしモードで準備。
	 * @throws IOException 
	 */
	@Before
	public void setUp() throws IOException {
		if (AppEngineUtil.isLocalDevelopment()) {
			TestUtil.setUpAppEngine("gae-j-sandbox", "gae-j-sandbox.1",
					"target/DatastoreServiceTest", true);
		} else {
//			logger.fine("ApiProxy.getDelegate()=" + ApiProxy.getDelegate());
			DatastoreServiceUtil.deleteKind("kind", 10);
			DatastoreServiceUtil.deleteKind("kind1", 10);
			DatastoreServiceUtil.deleteKind("kind2", 10);
			DatastoreServiceUtil.deleteKind("kind3", 10);
		}
	}

	/**
	 * 終了処理。
	 */
	@After
	public void tearDown() {
		if (AppEngineUtil.isLocalDevelopment()) {
			TestUtil.tearDownAppEngine();
		}
	}
}
