package appengine.util;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

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.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyRange;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Transaction;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.labs.taskqueue.Queue;
import com.google.appengine.api.labs.taskqueue.QueueFactory;
import com.google.appengine.api.labs.taskqueue.TaskOptions;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.memcache.Stats;

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

import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

/**
 * サービス名とメソッド名の確認用。
 * @author shin1ogawa
 */
public class LowLevelApiTest {

	/**
	 * task queue
	 * <p>存在しない名称で取得したQueueに{@link Queue#add()}した時に{@link IllegalStateException}が飛ぶ。
	 * {@link QueueFactory#getQueue(String)}のタイミングではExceptionは出ない(まだサービスへはアクセスしていないって事)。</p>
	 */
	@Test(expected = IllegalStateException.class)
	public void traceTaskQueue() {
		Queue defaultQueue = QueueFactory.getDefaultQueue();
		defaultQueue.add(TaskOptions.Builder.url("/")); // このタイミングでサービスにアクセス Add
		Queue notExistQueue = QueueFactory.getQueue("notExistQueueName");
		notExistQueue.add(TaskOptions.Builder.url("/")); // このタイミングでサービスにアクセス Add
	}

	/**
	 * {@link TestUtil#setUpAppEngineWithTaskQueueService(com.google.apphosting.api.ApiProxy.Environment, String, boolean, String)}
	 * を使ってdefault以外のqueueも単体テスト時に有効にする。
	 */
	@Test
	public void traceTaskQueue_default以外の名前指定() {
		Queue namedQueue = QueueFactory.getQueue("named");
		namedQueue.add(TaskOptions.Builder.url("/")); // このタイミングでサービスにアクセス Add
	}

	/**
	 * memcache service
	 */
	@Test
	public void traceMemcache() {
		MemcacheService service = MemcacheServiceFactory.getMemcacheService();
		service.put("key1", "hoge"); // Set
		Map<Object, Object> map = new HashMap<Object, Object>();
		map.put("key2", "fuga");
		map.put("key3", "fugafuga");
		service.putAll(map); // Set
		assertThat((String) service.get("key1"), is(equalTo("hoge"))); // Get
		assertThat((String) service.get("key2"), is(equalTo("fuga"))); // Get
		assertThat((String) service.get("key3"), is(equalTo("fugafuga"))); // Get
		service.delete("key3"); // Delete
		service.clearAll(); // FlushAll
	}

	/**
	 * memcache service
	 */
	@Test
	public void traceMemcacheDelete() {
		TraceLowLevelDelegate.setTrace(false);
		MemcacheService service = MemcacheServiceFactory.getMemcacheService();
		Map<Object, Object> map = new HashMap<Object, Object>();
		map.put("key2", "fuga");
		map.put("key3", "fugafuga");
		service.putAll(map); // Set
		TraceLowLevelDelegate.setTrace(true);
		service.deleteAll(map.keySet()); // Delete
	}

	/**
	 * memcache service
	 */
	@Test
	public void traceMemcacheOthers() {
		MemcacheService service = MemcacheServiceFactory.getMemcacheService();
		String namespace = service.getNamespace(); // サービスへのアクセスなし
		assertThat(namespace, is(equalTo("")));
		Stats statistics = service.getStatistics(); // Stats
		assertThat(statistics, is(notNullValue()));
		service.put("key1", 1);
		Long increment = service.increment("key1", 1L); // Increment
		assertThat(increment, is(equalTo(2L)));
	}

	/**
	 * datastore service
	 * @throws EntityNotFoundException
	 */
	@Test
	public void traceDatastore1() throws EntityNotFoundException {
		DatastoreService service = DatastoreServiceFactory.getDatastoreService();
		KeyRange allocateIds = service.allocateIds("kind", 1); // AllocateIds
		Key key = allocateIds.getStart();
		Entity entity = new Entity(key);
		service.put(entity); // Put
		Entity entity2 = service.get(key); // Get
		service.delete(key); // Delete

		Transaction transaction1 = service.beginTransaction(); // BeginTransaction
		Transaction transaction2 = service.getCurrentTransaction(); // サービスへのアクセスなし
		Collection<Transaction> activeTransactions = service.getActiveTransactions(); // サービスへのアクセスなし
		assertThat(activeTransactions.size(), is(equalTo(1)));
		assertThat(transaction1, is(equalTo(transaction2)));
		service.put(entity2); // Put
		transaction1.rollback(); // Rollback
		activeTransactions = service.getActiveTransactions(); // サービスへのアクセスなし
		assertThat(activeTransactions.size(), is(equalTo(0)));
		try {
			transaction2 = service.getCurrentTransaction(); // サービスへのアクセスなし
			fail();
		} catch (IllegalStateException e) {
			// getCurrentTransaction()に失敗する。
		}

		transaction1 = service.beginTransaction();
		transaction2 = service.beginTransaction();
		assertThat(service.getCurrentTransaction(), is(equalTo(transaction2))); // 最後にbeginしたTransaction
		activeTransactions = service.getActiveTransactions(); // サービスへのアクセスなし
		assertThat(activeTransactions.size(), is(equalTo(2)));
		for (Transaction transaction : activeTransactions) {
			if (transaction.isActive()) {
				transaction.commit(); // Commit Transaction内での操作が無くてもExceptionが飛ばない
			}
		}
	}

	/**
	 * {@link DatastoreService#get(Iterable)}
	 */
	@Test
	public void traceDatastoreGet() {
		TraceLowLevelDelegate.setTrace(false);
		Entity entity1 = new Entity("kind");
		entity1.setProperty("name", "kind(1)");
		Key key1 = DatastoreServiceUtil.putWithRetry(entity1, 5);
		Entity entity2 = new Entity("kind");
		entity2.setProperty("name", "kind(2)");
		Key key2 = DatastoreServiceUtil.putWithRetry(entity2, 5);
		TraceLowLevelDelegate.setTrace(true);
		DatastoreServiceFactory.getDatastoreService().get(Arrays.asList(key1, key2));
	}

	/**
	 * {@link DatastoreService#get(Iterable)}
	 * <p>EntityGroupの子に対するKeyだった場合</p>
	 */
	@Test
	public void traceDatastoreGet2() {
		TraceLowLevelDelegate.setTrace(false);
		Entity parent1 = new Entity("kind1");
		parent1.setProperty("name", "kind1(1)");
		Key parent1Key = DatastoreServiceUtil.putWithRetry(parent1, 5);
		Entity child1 = new Entity("kind2", parent1Key);
		child1.setProperty("name", "kind1(1)/kind2(1)");
		Key child1Key = DatastoreServiceUtil.putWithRetry(child1, 5);

		Entity parent2 = new Entity("kind1");
		parent2.setProperty("name", "kind1(2)");
		Key parent2Key = DatastoreServiceUtil.putWithRetry(parent2, 5);
		Entity child2 = new Entity("kind2", parent2Key);
		child2.setProperty("name", "kind1(2)/kind2(2)");
		Key child2Key = DatastoreServiceUtil.putWithRetry(child2, 5);

		TraceLowLevelDelegate.setTrace(true);
		DatastoreServiceFactory.getDatastoreService().get(Arrays.asList(child1Key, child2Key));
	}

	/**
	 * {@link PreparedQuery#countEntities()}
	 */
	@Test
	public void traceDatastoreCountEntities() {
		TraceLowLevelDelegate.setTrace(false);
		DatastoreService service = DatastoreServiceFactory.getDatastoreService();
		KeyRange allocateIds = service.allocateIds("kind1", 1);
		Key entity1Key = allocateIds.getStart();
		Entity entity1 = new Entity(entity1Key);
		entity1.setProperty("p1", "a");
		entity1.setUnindexedProperty("p2", "b");
		KeyRange allocateIds2 = service.allocateIds(entity1Key, "kind2", 1);
		Entity entity2 = new Entity(allocateIds2.getStart());
		entity2.setProperty("p1", "z");
		entity2.setUnindexedProperty("p2", "y");
		service.put(Arrays.asList(entity1, entity2));

		TraceLowLevelDelegate.setTrace(true);
		Query query1 = new Query("kind1").setKeysOnly();
		int count = service.prepare(query1).countEntities(); // Count
		assertThat(count, is(equalTo(1)));
	}

	/**
	 * datastore service
	 */
	@Test
	public void datastoreQuery() {
		TraceLowLevelDelegate.setTrace(false);
		DatastoreService service = DatastoreServiceFactory.getDatastoreService();
		KeyRange allocateIds = service.allocateIds("kind1", 1);
		Key entity1Key = allocateIds.getStart();
		Entity entity1 = new Entity(entity1Key);
		entity1.setProperty("p1", "a");
		entity1.setUnindexedProperty("p2", "b");
		KeyRange allocateIds2 = service.allocateIds(entity1Key, "kind2", 1);
		Entity entity2 = new Entity(allocateIds2.getStart());
		entity2.setProperty("p1", "z");
		entity2.setUnindexedProperty("p2", "y");
		service.put(Arrays.asList(entity1, entity2));
		TraceLowLevelDelegate.setTrace(true);

		Query query1 = new Query("kind1").setKeysOnly();
		Iterable<Entity> asIterable = service.prepare(query1).asIterable(); // サービスへのアクセスなし！
		asIterable.iterator(); // RunQuery このタイミングでサービスへアクセスする
		service.prepare(query1).asIterator(); // RunQuery
		service.prepare(query1).asList(FetchOptions.Builder.withOffset(0)); // RunQuery

		// ancestor key を使ったクエリも RunQuery. 何か特別なメソッドがあるわけではないようだ。
		Query query2 = new Query("kind2", entity1Key).setKeysOnly();
		asIterable = service.prepare(query2).asIterable(); // RunQuery
		asIterable.iterator(); // RunQuery 
		service.prepare(query2).asIterator(); // RunQuery
		service.prepare(query2).asList(FetchOptions.Builder.withOffset(0)); // RunQuery

		// setKeysOnly()が無くても特に違いはない。
		Query query3 = new Query("kind1").addFilter("p1", FilterOperator.EQUAL, "a");
		asIterable = service.prepare(query3).asIterable(); // RunQuery
		asIterable.iterator(); // RunQuery 
		service.prepare(query3).asIterator(); // RunQuery
		List<Entity> asList = service.prepare(query3).asList(FetchOptions.Builder.withOffset(0)); // RunQuery
		assertThat(asList.size(), is(equalTo(1)));

		// setUnindexProperty()したpropertyなのでソートできない(フィルタ条件にできない)
		Query query4 = new Query("kind1").addFilter("p2", FilterOperator.EQUAL, "b");
		asList = service.prepare(query4).asList(FetchOptions.Builder.withOffset(0)); // RunQuery
		assertThat(asList.size(), is(equalTo(0)));

		// [?] ancestorKeyを使った、対象Kindの指定をしないQuery。なーんも返ってこない。使い道がわからない。
		// 全Kindを横断してスキャンするのは難しい事とは思わないんだけど。
		//  Kind1(1)
		//  Kind1(1)/Kind2(1)
		//  Kind1(2)
		//  Kind1(2)/Kind2(2)
		// KindIndexに対してstartsWith()みたいな操作ができれば、簡単にできそーなのになぁ？
		// デプロイ環境では期待通りに動作する事が判明！
		Query query5 = new Query(entity1Key);
		asList = service.prepare(query5).asList(FetchOptions.Builder.withOffset(0)); // RunQuery
		if (AppEngineUtil.isDeployment()) {
			assertThat(asList.size(), is(equalTo(2)));
		} else {
			assertThat(asList.size(), is(equalTo(0))); // [?]
		}
	}


	static {
		Logger.getLogger("DataNucleus.Plugin").setLevel(Level.OFF);
	}


	/**
	 * ファイルへの保存なしで、低レベルAPIのトレースを行う設定をする。
	 * @throws IOException 
	 */
	@Before
	public void setUp() throws IOException {
		if (AppEngineUtil.isLocalDevelopment()) {
			TestUtil.setUpAppEngineWithTaskQueueService("gae-j-sandbox", "gae-j-sandbox.1",
					"target/LowLevelApiTest", true, "war/WEB-INF/queue.xml");
		}
		DatastoreServiceUtil.deleteKind("kind1", 100);
		DatastoreServiceUtil.deleteKind("kind2", 100);
		TraceLowLevelDelegate.delegateToTraceLowLevel();
	}

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