/*
 * Copyright (C) 2012 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 com.android.providers.contacts;

import static com.android.providers.contacts.TestUtils.cv;

import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Profile;
import android.provider.ContactsContract.RawContacts;

import androidx.test.filters.LargeTest;

import com.google.android.collect.Lists;

import java.util.ArrayList;

/**
 * Tests to make sure we're handling DB transactions properly in regard to two databases,
 * the profile db and the contacts db.
 */
@LargeTest
public class ContactsProvider2TransactionTest extends BaseContactsProvider2Test {
    private SynchronousContactsProvider2 mProvider;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        mProvider = (SynchronousContactsProvider2) getProvider();
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();

        mProvider = null;
    }

    /**
     * Make sure we start/finish transactions on the right databases for insert.
     */
    public void testTransactionCallback_insert() {

        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);

        // Insert a raw contact.
        mProvider.resetTrasactionCallbackCalledFlags();
        mResolver.insert(RawContacts.CONTENT_URI, values);

        // Make sure we only COMMIT on the contacts DB, but there was no transaction on the
        // profile db.
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertNoTransactionsForProfileMode();


        // Insert a profile raw contact.
        mProvider.resetTrasactionCallbackCalledFlags();
        mResolver.insert(Profile.CONTENT_RAW_CONTACTS_URI, values);

        // Even though we only touched the profile DB, we also start and finish a transaction
        // on the contacts db.  AbstractContactsProvider does that to avoid deadlocks.
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertCommitTransactionCalledForProfileMode();
    }

    /**
     * Make sure we start/finish transactions on the right databases for update.
     */
    public void testTransactionCallback_update() {

        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);

        // Make sure to create a raw contact and a profile raw contact.
        mResolver.insert(RawContacts.CONTENT_URI, values);
        mResolver.insert(Profile.CONTENT_RAW_CONTACTS_URI, values);

        values.clear();
        values.put(RawContacts.LAST_TIME_CONTACTED, 86400 * 2);

        // Update all raw contacts.
        mProvider.resetTrasactionCallbackCalledFlags();
        assertTrue(mResolver.update(RawContacts.CONTENT_URI, values, null, null) > 0);

        // Make sure we only COMMIT on the contacts DB, but there was no transaction on the
        // profile db.
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertNoTransactionsForProfileMode();


        // Update all profile raw contacts.
        mProvider.resetTrasactionCallbackCalledFlags();
        assertTrue(mResolver.update(Profile.CONTENT_RAW_CONTACTS_URI, values, null, null) > 0);

        // Even though we only touched the profile DB, we also start and finish a transaction
        // on the contacts db.  AbstractContactsProvider does that to avoid deadlocks.
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertCommitTransactionCalledForProfileMode();
    }

    /**
     * Make sure we start/finish transactions on the right databases for delete.
     */
    public void testTransactionCallback_delete() {

        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);

        // Make sure to create a raw contact and a profile raw contact.
        mResolver.insert(RawContacts.CONTENT_URI, values);
        mResolver.insert(Profile.CONTENT_RAW_CONTACTS_URI, values);

        // Delete all raw contacts.
        mProvider.resetTrasactionCallbackCalledFlags();
        assertTrue(mResolver.delete(RawContacts.CONTENT_URI, null, null) > 0);

        // Make sure we only COMMIT on the contacts DB, but there was no transaction on the
        // profile db.
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertNoTransactionsForProfileMode();

        // Delete all profile raw contact.
        mProvider.resetTrasactionCallbackCalledFlags();
        assertTrue(mResolver.delete(Profile.CONTENT_RAW_CONTACTS_URI, null, null) > 0);

        // Even though we only touched the profile DB, we also start and finish a transaction
        // on the contacts db.  AbstractContactsProvider does that to avoid deadlocks.
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertCommitTransactionCalledForProfileMode();
    }
    /**
     * Make sure we start/finish transactions on the right databases for bulk insert.
     */
    public void testTransactionCallback_bulkInsert() {

        final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);

        // Insert a raw contact.
        mProvider.resetTrasactionCallbackCalledFlags();
        mResolver.bulkInsert(RawContacts.CONTENT_URI, new ContentValues[] {values});

        // Make sure we only COMMIT on the contacts DB, but there was no transaction on the
        // profile db.
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertNoTransactionsForProfileMode();


        // Insert a profile raw contact.
        mProvider.resetTrasactionCallbackCalledFlags();
        mResolver.bulkInsert(Profile.CONTENT_RAW_CONTACTS_URI, new ContentValues[] {values});

        // Even though we only touched the profile DB, we also start and finish a transaction
        // on the contacts db.  AbstractContactsProvider does that to avoid deadlocks.
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertCommitTransactionCalledForProfileMode();
    }

    /**
     * Add an operation to create a raw contact.
     */
    private static void addInsertContactOperations(ArrayList<ContentProviderOperation> ops) {
        ContentProviderOperation.Builder b;
        b = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
        b.withValue(RawContacts.STARRED, 1);
        b.withValue(RawContacts.LAST_TIME_CONTACTED, 86400 * 21);
        ops.add(b.build());

        b = ContentProviderOperation.newInsert(Data.CONTENT_URI);
        b.withValueBackReference(Data.RAW_CONTACT_ID, ops.size() - 1);
        b.withValue(StructuredName.DISPLAY_NAME, "Regular Contact");
        b.withValue(StructuredName.GIVEN_NAME, "Regular");
        b.withValue(StructuredName.FAMILY_NAME, "Contact");
        b.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
        ops.add(b.build());
    }

    /**
     * Check for a contact created that'll be created for {@link #addInsertContactOperations}.
     */
    private void checkStoredContact() {
        assertStoredValues(Contacts.CONTENT_URI, cv(
                Contacts.DISPLAY_NAME, "Regular Contact",
                RawContacts.LAST_TIME_CONTACTED, 0
                ));
    }

    /**
     * Add an operation to create a profile raw contact.
     */
    private static void addInsertProfileOperations(ArrayList<ContentProviderOperation> ops) {
        ContentProviderOperation.Builder b;
        b = ContentProviderOperation.newInsert(Profile.CONTENT_RAW_CONTACTS_URI);
        b.withValue(RawContacts.STARRED, 1);
        b.withValue(RawContacts.LAST_TIME_CONTACTED, 86400 * 11);
        ops.add(b.build());

        b = ContentProviderOperation.newInsert(Data.CONTENT_URI);
        b.withValueBackReference(Data.RAW_CONTACT_ID, ops.size() - 1);
        b.withValue(StructuredName.DISPLAY_NAME, "Profile Contact");
        b.withValue(StructuredName.GIVEN_NAME, "Profile");
        b.withValue(StructuredName.FAMILY_NAME, "Contact");
        b.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
        ops.add(b.build());
    }

    /**
     * Check for a profile contact created that'll be created for
     * {@link #addInsertProfileOperations}.
     */
    private void checkStoredProfile() {
        assertStoredValues(Profile.CONTENT_URI, cv(
                Contacts.DISPLAY_NAME, "Profile Contact",
                RawContacts.LAST_TIME_CONTACTED, 0
                ));
    }

    public void testTransactionCallback_contactBatch() throws Exception {
        final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();

        addInsertContactOperations(ops);

        mProvider.resetTrasactionCallbackCalledFlags();

        // Execute the operations.
        mResolver.applyBatch(ContactsContract.AUTHORITY, ops);

        // Check the result
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertNoTransactionsForProfileMode();

        checkStoredContact();
    }

    public void testTransactionCallback_profileBatch() throws Exception {
        final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();

        addInsertProfileOperations(ops);

        mProvider.resetTrasactionCallbackCalledFlags();

        // Execute the operations.
        mResolver.applyBatch(ContactsContract.AUTHORITY, ops);

        // Check the result
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertCommitTransactionCalledForProfileMode();

        checkStoredProfile();
    }

    public void testTransactionCallback_mixedBatch() throws Exception {
        final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();

        // Create a raw contact and a profile raw contact in a single batch.

        addInsertContactOperations(ops);
        addInsertProfileOperations(ops);

        mProvider.resetTrasactionCallbackCalledFlags();

        // Execute the operations.
        mResolver.applyBatch(ContactsContract.AUTHORITY, ops);

        // Check the result
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertCommitTransactionCalledForProfileMode();

        checkStoredProfile();
        checkStoredContact();
    }

    public void testTransactionCallback_mixedBatchReversed() throws Exception {
        final ArrayList<ContentProviderOperation> ops = Lists.newArrayList();

        // Create a profile raw contact and a raw contact in a single batch.

        addInsertProfileOperations(ops);
        addInsertContactOperations(ops);

        mProvider.resetTrasactionCallbackCalledFlags();

        // Execute the operations.
        mResolver.applyBatch(ContactsContract.AUTHORITY, ops);

        // Check the result
        mProvider.assertCommitTransactionCalledForContactMode();
        mProvider.assertCommitTransactionCalledForProfileMode();

        checkStoredProfile();
        checkStoredContact();
    }
}
