/*
 * Copyright 2013 Google Inc. All Rights Reserved.
 * 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
 *
 *   http://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 com.google.appengine.tck.datastore;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.google.appengine.api.datastore.AsyncDatastoreService;
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.KeyFactory;
import com.google.appengine.api.datastore.KeyRange;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.PropertyProjection;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterPredicate;
import com.google.appengine.tck.base.TestBase;
import org.apache.commons.codec.BinaryDecoder;
import org.apache.commons.codec.BinaryEncoder;
import org.apache.commons.codec.Decoder;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.Encoder;
import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.BaseNCodec;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;

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

/**
 * Datastore test helper
 */
public abstract class DatastoreHelperTestBase extends TestBase {
    protected String rootKind = "root";
    protected Key rootKey = null;
    protected String propertyName = "propName";
    protected DatastoreService service;
    protected int waitTime = 2000;

    protected static WebArchive getHelperDeployment() {
        WebArchive war = getTckDeployment();
        war.addClass(DatastoreHelperTestBase.class)
            .addClasses(Base64.class, BaseNCodec.class)
            .addClasses(BinaryEncoder.class, Encoder.class)
            .addClasses(BinaryDecoder.class, Decoder.class)
            .addClasses(EncoderException.class, DecoderException.class);
        return war;
    }

    @Before
    public void setUp() {
        try {
            service = DatastoreServiceFactory.getDatastoreService();
            ensureRootEntityExists();
        } catch (InterruptedException ie) {
            throw new IllegalStateException(ie);
        }
    }

    @After
    public void tearDown() {
        service = null;
    }

    protected FetchOptions withDefaults() {
        return FetchOptions.Builder.withDefaults();
    }

    protected void clearData(String kind) {
        clearData(kind, rootKey, waitTime);
    }

    protected void clearData(String kind, Key parentKey, int waitMilliSec) {
        List<Key> eList = new ArrayList<>();
        Query query = new Query(kind, parentKey);
        for (Entity readRec : service.prepare(query).asIterable()) {
            eList.add(readRec.getKey());
        }
        if (eList.size() > 0) {
            service.delete(eList);
            sync(waitMilliSec);
        }
    }

    protected Object[] getResult(Query query, String pName) {
        int count = service.prepare(query).countEntities(FetchOptions.Builder.withDefaults());
        Object result[] = new Object[count];
        int pt = 0;
        for (Entity readRec : service.prepare(query).asIterable()) {
            result[pt++] = readRec.getProperty(pName);
        }
        return result;
    }

    protected void doAllFilters(String kind, String pName, Object expected) {
        doNonEqFilters(kind, pName, expected);
        doEqFilters(kind, pName, expected);
    }

    protected void doNonEqFilters(String kind, String pName, Object expected) {
        verifyFilter(kind, pName, expected, Query.FilterOperator.LESS_THAN, 1, false);
        verifyFilter(kind, pName, expected, Query.FilterOperator.GREATER_THAN, 1, false);
    }

    protected void doEqFilters(String kind, String pName, Object expected) {
        verifyFilter(kind, pName, expected, Query.FilterOperator.LESS_THAN_OR_EQUAL, 2, true);
        verifyFilter(kind, pName, expected, Query.FilterOperator.EQUAL, 1, true);
        verifyFilter(kind, pName, expected, Query.FilterOperator.GREATER_THAN_OR_EQUAL, 2, true);
    }

    protected void doEqOnlyFilter(String kind, String pName, Object expected) {
        verifyFilter(kind, pName, expected, Query.FilterOperator.EQUAL, 1, true);
    }

    /**
     * inChk
     * - true: check if fDat are in result and if result count is correct;
     * - false: only check if result count is correct
     */
    protected void verifyFilter(String kind, String pName, Object fDat,
                                Query.FilterOperator operator, int rCont, boolean inChk) {
        Query query = new Query(kind, rootKey);
        query.setFilter(new FilterPredicate(pName, operator, fDat));
        Object[] result = getResult(query, pName);
        assertEquals(rCont, result.length);
        if (inChk) {
            boolean find = false;
            for (Object data : result) {
                if (data.toString().equals(fDat.toString())) {
                    find = true;
                }
            }
            assertEquals(true, find);
        }
    }

    protected void doSort(String kind, String pName, Object fDat, Query.SortDirection direction) {
        Query query = new Query(kind, rootKey);
        query.addSort(pName, direction);
        Object[] result = getResult(query, pName);
        assertEquals(fDat.toString(), result[0].toString());
    }

//    protected void doSort(String kind, String pName, Object[] expDat, Query.SortDirection direction) {
//        Query query = new Query(kind, rootKey);
//        query.addSort(pName, direction);
//        Object[] result = getResult(query, pName);
//        assertEquals(expDat.length, result.length);
//        assertTrue(Arrays.equals(result, expDat));
//    }

    protected void doSort(String kind, String pName, int expDat, Query.SortDirection direction) {
        Query query = new Query(kind, rootKey);
        query.addSort(pName, direction);
        Object[] result = getResult(query, pName);
        assertEquals(expDat, result.length);
    }

    protected void ensureRootEntityExists() throws InterruptedException {
        Query query = new Query(rootKind);

        // Clean up previous run
        List<Entity> entities = service.prepare(query).asList(withDefaults());
        for (Entity entity : entities) {
            service.delete(entity.getKey());
        }

        Entity rootEntity = service.prepare(query).asSingleEntity();
        if (rootEntity == null) {
            rootEntity = new Entity(rootKind);
            rootEntity.setProperty("name", "junittests group");
            rootKey = service.put(rootEntity);
            sync(waitTime);
        } else {
            rootKey = rootEntity.getKey();
        }
    }

    protected Collection<Entity> createTestEntities() {
        return Arrays.asList(createTestEntity("One", 1), createTestEntity("Two", 2), createTestEntity("Three", 3));
    }

    protected void assertStoreContainsAll(Collection<Entity> entities) throws EntityNotFoundException {
        for (Entity entity : entities) {
            assertStoreContains(entity);
        }
    }

    protected void assertStoreContains(Entity entity) throws EntityNotFoundException {
        Entity lookup = service.get(entity.getKey());
        Assert.assertNotNull(lookup);
        Assert.assertEquals(entity, lookup);
    }

    protected void assertStoreDoesNotContain(Entity entity) throws EntityNotFoundException {
        assertStoreDoesNotContain(entity.getKey());
    }

    protected void assertStoreDoesNotContain(Collection<Key> keys) throws EntityNotFoundException {
        for (Key key : keys) {
            assertStoreDoesNotContain(key);
        }
    }

    protected void assertStoreDoesNotContain(Key key) throws EntityNotFoundException {
        try {
            Entity storedEntity = service.get(key);
            Assert.fail("expected the datastore not to contain anything under key " + key + ", but it contained the entity " + storedEntity);
        } catch (EntityNotFoundException e) {
            // pass
        }
    }

    protected void assertEntityNotInRange(Entity entity, KeyRange range) {
        // allocated key should not be re-used.
        Assert.assertTrue(entity.getKey().getId() > range.getEnd().getId() ||
            entity.getKey().getId() < range.getStart().getId());
    }

    protected Entity createTestEntity() {
        return createTestEntity("KIND");
    }

    protected Entity createTestEntity(String kind) {
        Entity entity = new Entity(kind);
        entity.setProperty("text", "Some text.");
        return entity;
    }

    protected Entity createTestEntity(String kind, long id) {
        Key key = KeyFactory.createKey(kind, id);
        Entity entity = new Entity(key);
        entity.setProperty("text", "Some text.");
        return entity;
    }

    protected Entity createTestEntity(String kind, Key key) {
        Entity entity = new Entity(kind, key);
        entity.setProperty("text", "Some text.");
        return entity;
    }

    protected Entity createTestEntityWithUniqueMethodNameKey(String kind, String testMethodName) {
        String key = testMethodName + "-" + System.currentTimeMillis();
        return new Entity(kind, key);
    }

    protected void assertIAEWhenAccessingResult(PreparedQuery preparedQuery) {
        assertIAEWhenAccessingList(preparedQuery);
        assertIAEWhenAccessingIterator(preparedQuery);
        assertIAEWhenAccessingIterable(preparedQuery);
        assertIAEWhenGettingSingleEntity(preparedQuery);
    }

    protected List<Entity> doQuery(String kind, String pName, Class<?> type, boolean indexed) {
        FetchOptions fo = FetchOptions.Builder.withDefaults();
        Query query = new Query(kind, rootKey);
        if (indexed) {
            query.addProjection(new PropertyProjection(pName, type));
            query.addSort(pName);
        }
        return service.prepare(query).asList(fo);
    }

    protected Entity getSingleEntity(DatastoreService ds, Key key) {
        Map<Key, Entity> map = ds.get(Collections.singleton(key));
        return (map.isEmpty() ? null : map.values().iterator().next());
    }

    protected Entity getSingleEntity(AsyncDatastoreService ds, Key key) {
        Map<Key, Entity> map = waitOnFuture(ds.get(Collections.singleton(key)));
        return (map.isEmpty() ? null : map.values().iterator().next());
    }

    private void assertIAEWhenAccessingList(PreparedQuery preparedQuery) {
        List<Entity> list = preparedQuery.asList(withDefaults());
        try {
            list.size();
            fail("Expected IllegalArgumentException");
        } catch (IllegalArgumentException ex) {
            // pass
        }
    }

    private void assertIAEWhenAccessingIterator(PreparedQuery preparedQuery) {
        Iterator<Entity> iterator = preparedQuery.asIterator();
        try {
            iterator.hasNext();
            fail("Expected IllegalArgumentException");
        } catch (IllegalArgumentException ex) {
            // pass
        }
    }

    private void assertIAEWhenAccessingIterable(PreparedQuery preparedQuery) {
        Iterator<Entity> iterator2 = preparedQuery.asIterable().iterator();
        try {
            iterator2.hasNext();
            fail("Expected IllegalArgumentException");
        } catch (IllegalArgumentException ex) {
            // pass
        }
    }

    private void assertIAEWhenGettingSingleEntity(PreparedQuery preparedQuery) {
        try {
            preparedQuery.asSingleEntity();
            fail("Expected IllegalArgumentException");
        } catch (IllegalArgumentException ex) {
            // pass
        }
    }
}