/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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 androidx.room.integration.testapp.test;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteException;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;

import androidx.room.Room;
import androidx.room.integration.testapp.TestDatabase;
import androidx.room.integration.testapp.dao.BlobEntityDao;
import androidx.room.integration.testapp.dao.PetDao;
import androidx.room.integration.testapp.dao.ProductDao;
import androidx.room.integration.testapp.dao.UserDao;
import androidx.room.integration.testapp.dao.UserPetDao;
import androidx.room.integration.testapp.vo.BlobEntity;
import androidx.room.integration.testapp.vo.Day;
import androidx.room.integration.testapp.vo.NameAndLastName;
import androidx.room.integration.testapp.vo.Pet;
import androidx.room.integration.testapp.vo.Product;
import androidx.room.integration.testapp.vo.User;
import androidx.room.integration.testapp.vo.UserAndAllPets;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
@SmallTest
@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
    private UserDao mUserDao;
    private BlobEntityDao mBlobEntityDao;
    private PetDao mPetDao;
    private UserPetDao mUserPetDao;
    private ProductDao mProductDao;

    @Before
    public void createDb() {
        Context context = InstrumentationRegistry.getTargetContext();
        TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
        mUserDao = db.getUserDao();
        mPetDao = db.getPetDao();
        mUserPetDao = db.getUserPetDao();
        mBlobEntityDao = db.getBlobEntityDao();
        mProductDao = db.getProductDao();
    }

    @Test
    public void writeUserAndReadInList() throws Exception {
        User user = TestUtil.createUser(3);
        user.setName("george");
        mUserDao.insert(user);
        List<User> byName = mUserDao.findUsersByName("george");
        assertThat(byName.get(0), equalTo(user));
    }

    @Test
    public void insertNull() throws Exception {
        @SuppressWarnings("ConstantConditions")
        Product product = new Product(1, null);
        Throwable throwable = null;
        try {
            mProductDao.insert(product);
        } catch (Throwable t) {
            throwable = t;
        }
        assertNotNull("Was expecting an exception", throwable);
        assertThat(throwable, instanceOf(SQLiteConstraintException.class));
    }

    @Test
    public void insertDifferentEntities() throws Exception {
        User user1 = TestUtil.createUser(3);
        user1.setName("george");
        Pet pet = TestUtil.createPet(1);
        pet.setUserId(3);
        pet.setName("a");
        mUserPetDao.insertUserAndPet(user1, pet);
        assertThat(mUserDao.count(), is(1));
        List<UserAndAllPets> inserted = mUserPetDao.loadAllUsersWithTheirPets();
        assertThat(inserted, hasSize(1));
        assertThat(inserted.get(0).user.getId(), is(3));
        assertThat(inserted.get(0).user.getName(), is(equalTo("george")));
        assertThat(inserted.get(0).pets, hasSize(1));
        assertThat(inserted.get(0).pets.get(0).getPetId(), is(1));
        assertThat(inserted.get(0).pets.get(0).getName(), is("a"));
        assertThat(inserted.get(0).pets.get(0).getUserId(), is(3));
        pet.setName("b");
        mUserPetDao.updateUsersAndPets(new User[]{user1}, new Pet[]{pet});
        List<UserAndAllPets> updated = mUserPetDao.loadAllUsersWithTheirPets();
        assertThat(updated, hasSize(1));
        assertThat(updated.get(0).pets, hasSize(1));
        assertThat(updated.get(0).pets.get(0).getName(), is("b"));
        User user2 = TestUtil.createUser(5);
        user2.setName("chet");
        mUserDao.insert(user2);
        assertThat(mUserDao.count(), is(2));
        mUserPetDao.delete2UsersAndPets(user1, user2, new Pet[]{pet});
        List<UserAndAllPets> deleted = mUserPetDao.loadAllUsersWithTheirPets();
        assertThat(deleted, hasSize(0));
    }

    @Test
    public void insertDifferentEntities_transaction() throws Exception {
        Pet pet = TestUtil.createPet(1);
        mPetDao.insertOrReplace(pet);
        assertThat(mPetDao.count(), is(1));
        User user = TestUtil.createUser(3);
        try {
            mUserPetDao.insertUserAndPet(user, pet);
            fail("Exception expected");
        } catch (SQLiteConstraintException ignored) {
        }
        assertThat(mUserDao.count(), is(0));
        assertThat(mPetDao.count(), is(1));
    }

    @Test
    public void throwExceptionOnConflict() {
        User user = TestUtil.createUser(3);
        mUserDao.insert(user);

        User user2 = TestUtil.createUser(3);
        try {
            mUserDao.insert(user2);
            throw new AssertionError("didn't throw in conflicting insertion");
        } catch (SQLiteException ignored) {
        }
    }

    @Test
    public void replaceOnConflict() {
        User user = TestUtil.createUser(3);
        mUserDao.insert(user);

        User user2 = TestUtil.createUser(3);
        mUserDao.insertOrReplace(user2);

        assertThat(mUserDao.load(3), equalTo(user2));
        assertThat(mUserDao.load(3), not(equalTo(user)));
    }

    @Test
    public void updateSimple() {
        User user = TestUtil.createUser(3);
        mUserDao.insert(user);
        user.setName("i am an updated name");
        assertThat(mUserDao.update(user), is(1));
        assertThat(mUserDao.load(user.getId()), equalTo(user));
    }

    @Test
    public void updateNonExisting() {
        User user = TestUtil.createUser(3);
        mUserDao.insert(user);
        User user2 = TestUtil.createUser(4);
        assertThat(mUserDao.update(user2), is(0));
    }

    @Test
    public void updateList() {
        List<User> users = TestUtil.createUsersList(3, 4, 5);
        mUserDao.insertAll(users.toArray(new User[3]));
        for (User user : users) {
            user.setName("name " + user.getId());
        }
        assertThat(mUserDao.updateAll(users), is(3));
        for (User user : users) {
            assertThat(mUserDao.load(user.getId()).getName(), is("name " + user.getId()));
        }
    }

    @Test
    public void updateListPartial() {
        List<User> existingUsers = TestUtil.createUsersList(3, 4, 5);
        mUserDao.insertAll(existingUsers.toArray(new User[3]));
        for (User user : existingUsers) {
            user.setName("name " + user.getId());
        }
        List<User> allUsers = TestUtil.createUsersList(7, 8, 9);
        allUsers.addAll(existingUsers);
        assertThat(mUserDao.updateAll(allUsers), is(3));
        for (User user : existingUsers) {
            assertThat(mUserDao.load(user.getId()).getName(), is("name " + user.getId()));
        }
    }

    @Test
    public void delete() {
        User user = TestUtil.createUser(3);
        mUserDao.insert(user);
        assertThat(mUserDao.delete(user), is(1));
        assertThat(mUserDao.delete(user), is(0));
        assertThat(mUserDao.load(3), is(nullValue()));
    }

    @Test
    public void deleteAll() {
        User[] users = TestUtil.createUsersArray(3, 5, 7, 9);
        mUserDao.insertAll(users);
        // there is actually no guarantee for this order by works fine since they are ordered for
        // the test and it is a new database (no pages to recycle etc)
        assertThat(mUserDao.loadByIds(3, 5, 7, 9), is(users));
        int deleteCount = mUserDao.deleteAll(new User[]{users[0], users[3],
                TestUtil.createUser(9)});
        assertThat(deleteCount, is(2));
        assertThat(mUserDao.loadByIds(3, 5, 7, 9), is(new User[]{users[1], users[2]}));
    }

    @Test
    public void deleteEverything() {
        User user = TestUtil.createUser(3);
        mUserDao.insert(user);
        assertThat(mUserDao.count(), is(1));
        int count = mUserDao.deleteEverything();
        assertThat(count, is(1));
        assertThat(mUserDao.count(), is(0));
    }

    @Test
    public void findByBoolean() {
        User user1 = TestUtil.createUser(3);
        user1.setAdmin(true);
        User user2 = TestUtil.createUser(5);
        user2.setAdmin(false);
        mUserDao.insert(user1);
        mUserDao.insert(user2);
        assertThat(mUserDao.findByAdmin(true), is(Arrays.asList(user1)));
        assertThat(mUserDao.findByAdmin(false), is(Arrays.asList(user2)));
    }

    @Test
    public void returnBoolean() {
        User user1 = TestUtil.createUser(1);
        User user2 = TestUtil.createUser(2);
        user1.setAdmin(true);
        user2.setAdmin(false);
        mUserDao.insert(user1);
        mUserDao.insert(user2);
        assertThat(mUserDao.isAdmin(1), is(true));
        assertThat(mUserDao.isAdmin(2), is(false));
    }

    @Test
    public void findByCollateNoCase() {
        User user = TestUtil.createUser(3);
        user.setCustomField("abc");
        mUserDao.insert(user);
        List<User> users = mUserDao.findByCustomField("ABC");
        assertThat(users, hasSize(1));
        assertThat(users.get(0).getId(), is(3));
    }

    @Test
    public void deleteByAge() {
        User user1 = TestUtil.createUser(3);
        user1.setAge(30);
        User user2 = TestUtil.createUser(5);
        user2.setAge(45);
        mUserDao.insert(user1);
        mUserDao.insert(user2);
        assertThat(mUserDao.deleteAgeGreaterThan(60), is(0));
        assertThat(mUserDao.deleteAgeGreaterThan(45), is(0));
        assertThat(mUserDao.deleteAgeGreaterThan(35), is(1));
        assertThat(mUserDao.loadByIds(3, 5), is(new User[]{user1}));
    }

    @Test
    public void deleteByAgeRange() {
        User user1 = TestUtil.createUser(3);
        user1.setAge(30);
        User user2 = TestUtil.createUser(5);
        user2.setAge(45);
        mUserDao.insert(user1);
        mUserDao.insert(user2);
        assertThat(mUserDao.deleteByAgeRange(35, 40), is(0));
        assertThat(mUserDao.deleteByAgeRange(25, 30), is(1));
        assertThat(mUserDao.loadByIds(3, 5), is(new User[]{user2}));
    }

    @Test
    public void deleteByUIds() {
        User[] users = TestUtil.createUsersArray(3, 5, 7, 9, 11);
        mUserDao.insertAll(users);
        assertThat(mUserDao.deleteByUids(2, 4, 6), is(0));
        assertThat(mUserDao.deleteByUids(3, 11), is(2));
        assertThat(mUserDao.loadByIds(3, 5, 7, 9, 11), is(new User[]{
                users[1], users[2], users[3]
        }));
    }

    @Test
    public void updateNameById() {
        User[] usersArray = TestUtil.createUsersArray(3, 5, 7);
        mUserDao.insertAll(usersArray);
        assertThat("test sanity", usersArray[1].getName(), not(equalTo("updated name")));
        int changed = mUserDao.updateById(5, "updated name");
        assertThat(changed, is(1));
        assertThat(mUserDao.load(5).getName(), is("updated name"));
    }

    @Test
    public void incrementIds() {
        User[] usersArr = TestUtil.createUsersArray(2, 4, 6);
        mUserDao.insertAll(usersArr);
        mUserDao.incrementIds(1);
        assertThat(mUserDao.loadIds(), is(Arrays.asList(3, 5, 7)));
    }

    @Test
    public void incrementAgeOfAll() {
        User[] users = TestUtil.createUsersArray(3, 5, 7);
        users[0].setAge(3);
        users[1].setAge(5);
        users[2].setAge(7);
        mUserDao.insertAll(users);
        assertThat(mUserDao.count(), is(3));
        mUserDao.incrementAgeOfAll();
        for (User user : mUserDao.loadByIds(3, 5, 7)) {
            assertThat(user.getAge(), is(user.getId() + 1));
        }
    }

    @Test
    public void findByIntQueryParameter() {
        User user = TestUtil.createUser(1);
        final String name = "my name";
        user.setName(name);
        mUserDao.insert(user);
        assertThat(mUserDao.findByNameLength(name.length()), is(Collections.singletonList(user)));
    }

    @Test
    public void findByIntFieldMatch() {
        User user = TestUtil.createUser(1);
        user.setAge(19);
        mUserDao.insert(user);
        assertThat(mUserDao.findByAge(19), is(Collections.singletonList(user)));
    }

    @Test
    public void customConverterField() {
        User user = TestUtil.createUser(20);
        Date theDate = new Date(System.currentTimeMillis() - 200);
        user.setBirthday(theDate);
        mUserDao.insert(user);
        assertThat(mUserDao.findByBirthdayRange(new Date(theDate.getTime() - 100),
                new Date(theDate.getTime() + 1)).get(0), is(user));
        assertThat(mUserDao.findByBirthdayRange(new Date(theDate.getTime()),
                new Date(theDate.getTime() + 1)).size(), is(0));
    }

    @Test
    public void renamedField() {
        User user = TestUtil.createUser(3);
        user.setCustomField("foo laaa");
        mUserDao.insertOrReplace(user);
        User loaded = mUserDao.load(3);
        assertThat(loaded.getCustomField(), is("foo laaa"));
        assertThat(loaded, is(user));
    }

    @Test
    public void readViaCursor() {
        User[] users = TestUtil.createUsersArray(3, 5, 7, 9);
        mUserDao.insertAll(users);
        Cursor cursor = mUserDao.findUsersAsCursor(3, 5, 9);
        try {
            assertThat(cursor.getCount(), is(3));
            assertThat(cursor.moveToNext(), is(true));
            assertThat(cursor.getInt(0), is(3));
            assertThat(cursor.moveToNext(), is(true));
            assertThat(cursor.getInt(0), is(5));
            assertThat(cursor.moveToNext(), is(true));
            assertThat(cursor.getInt(0), is(9));
            assertThat(cursor.moveToNext(), is(false));
        } finally {
            cursor.close();
        }
    }

    @Test
    public void readDirectWithTypeAdapter() {
        User user = TestUtil.createUser(3);
        user.setBirthday(null);
        mUserDao.insert(user);
        assertThat(mUserDao.getBirthday(3), is(nullValue()));
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.YEAR, 3);
        Date birthday = calendar.getTime();
        user.setBirthday(birthday);

        mUserDao.update(user);
        assertThat(mUserDao.getBirthday(3), is(birthday));
    }

    @Test
    public void emptyInQuery() {
        User[] users = mUserDao.loadByIds();
        assertThat(users, is(new User[0]));
    }

    @Test
    public void blob() {
        BlobEntity a = new BlobEntity(1, "abc".getBytes());
        BlobEntity b = new BlobEntity(2, "def".getBytes());
        mBlobEntityDao.insert(a);
        mBlobEntityDao.insert(b);
        List<BlobEntity> list = mBlobEntityDao.selectAll();
        assertThat(list, hasSize(2));
        mBlobEntityDao.updateContent(2, "ghi".getBytes());
        assertThat(mBlobEntityDao.getContent(2), is(equalTo("ghi".getBytes())));
    }

    @Test
    public void transactionByRunnable() {
        User a = TestUtil.createUser(3);
        User b = TestUtil.createUser(5);
        mUserDao.insertBothByRunnable(a, b);
        assertThat(mUserDao.count(), is(2));
    }

    @Test
    public void transactionByRunnable_failure() {
        User a = TestUtil.createUser(3);
        User b = TestUtil.createUser(3);
        boolean caught = false;
        try {
            mUserDao.insertBothByRunnable(a, b);
        } catch (SQLiteConstraintException e) {
            caught = true;
        }
        assertTrue("SQLiteConstraintException expected", caught);
        assertThat(mUserDao.count(), is(0));
    }

    @Test
    public void transactionByCallable() {
        User a = TestUtil.createUser(3);
        User b = TestUtil.createUser(5);
        int count = mUserDao.insertBothByCallable(a, b);
        assertThat(mUserDao.count(), is(2));
        assertThat(count, is(2));
    }

    @Test
    public void transactionByCallable_failure() {
        User a = TestUtil.createUser(3);
        User b = TestUtil.createUser(3);
        boolean caught = false;
        try {
            mUserDao.insertBothByCallable(a, b);
        } catch (SQLiteConstraintException e) {
            caught = true;
        }
        assertTrue("SQLiteConstraintException expected", caught);
        assertThat(mUserDao.count(), is(0));
    }

    @Test
    public void transactionByDefaultImplementation() {
        Pet pet1 = TestUtil.createPet(1);
        mPetDao.insertOrReplace(pet1);
        assertThat(mPetDao.count(), is(1));
        assertThat(mPetDao.allIds()[0], is(1));
        Pet pet2 = TestUtil.createPet(2);
        mPetDao.deleteAndInsert(pet1, pet2, false);
        assertThat(mPetDao.count(), is(1));
        assertThat(mPetDao.allIds()[0], is(2));
    }

    @Test
    public void transactionByDefaultImplementation_failure() {
        Pet pet1 = TestUtil.createPet(1);
        mPetDao.insertOrReplace(pet1);
        assertThat(mPetDao.count(), is(1));
        assertThat(mPetDao.allIds()[0], is(1));
        Pet pet2 = TestUtil.createPet(2);
        Throwable throwable = null;
        try {
            mPetDao.deleteAndInsert(pet1, pet2, true);
        } catch (Throwable t) {
            throwable = t;
        }
        assertNotNull("Was expecting an exception", throwable);
        assertThat(mPetDao.count(), is(1));
        assertThat(mPetDao.allIds()[0], is(1));
    }

    @Test
    public void multipleInParamsFollowedByASingleParam_delete() {
        User user = TestUtil.createUser(3);
        user.setAge(30);
        mUserDao.insert(user);
        assertThat(mUserDao.deleteByAgeAndIds(20, Arrays.asList(3, 5)), is(0));
        assertThat(mUserDao.count(), is(1));
        assertThat(mUserDao.deleteByAgeAndIds(30, Arrays.asList(3, 5)), is(1));
        assertThat(mUserDao.count(), is(0));
    }

    @Test
    public void multipleInParamsFollowedByASingleParam_update() {
        User user = TestUtil.createUser(3);
        user.setAge(30);
        user.setWeight(10f);
        mUserDao.insert(user);
        assertThat(mUserDao.updateByAgeAndIds(3f, 20, Arrays.asList(3, 5)), is(0));
        assertThat(mUserDao.loadByIds(3)[0].getWeight(), is(10f));
        assertThat(mUserDao.updateByAgeAndIds(3f, 30, Arrays.asList(3, 5)), is(1));
        assertThat(mUserDao.loadByIds(3)[0].getWeight(), is(3f));
    }

    @Test
    public void transactionByAnnotation() {
        User a = TestUtil.createUser(3);
        User b = TestUtil.createUser(5);
        mUserDao.insertBothByAnnotation(a, b);
        assertThat(mUserDao.count(), is(2));
    }

    @Test
    public void transactionByAnnotation_failure() {
        User a = TestUtil.createUser(3);
        User b = TestUtil.createUser(3);
        boolean caught = false;
        try {
            mUserDao.insertBothByAnnotation(a, b);
        } catch (SQLiteConstraintException e) {
            caught = true;
        }
        assertTrue("SQLiteConstraintException expected", caught);
        assertThat(mUserDao.count(), is(0));
    }

    @Test
    public void tablePrefix_simpleSelect() {
        User user = TestUtil.createUser(3);
        mUserDao.insert(user);
        NameAndLastName result = mUserDao.getNameAndLastName(3);
        assertThat(result.getName(), is(user.getName()));
        assertThat(result.getLastName(), is(user.getLastName()));
    }

    @Test
    public void enumSet_simpleLoad() {
        User a = TestUtil.createUser(3);
        Set<Day> expected = toSet(Day.MONDAY, Day.TUESDAY);
        a.setWorkDays(expected);
        mUserDao.insert(a);
        User loaded = mUserDao.load(3);
        assertThat(loaded.getWorkDays(), is(expected));
    }

    @Test
    public void enumSet_query() {
        User user1 = TestUtil.createUser(3);
        user1.setWorkDays(toSet(Day.MONDAY, Day.FRIDAY));
        User user2 = TestUtil.createUser(5);
        user2.setWorkDays(toSet(Day.MONDAY, Day.THURSDAY));
        mUserDao.insert(user1);
        mUserDao.insert(user2);
        List<User> empty = mUserDao.findUsersByWorkDays(toSet(Day.WEDNESDAY));
        assertThat(empty.size(), is(0));
        List<User> friday = mUserDao.findUsersByWorkDays(toSet(Day.FRIDAY));
        assertThat(friday, is(Arrays.asList(user1)));
        List<User> monday = mUserDao.findUsersByWorkDays(toSet(Day.MONDAY));
        assertThat(monday, is(Arrays.asList(user1, user2)));

    }

    private Set<Day> toSet(Day... days) {
        return new HashSet<>(Arrays.asList(days));
    }
}
