/*
 * Copyright (C) 2009 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.contacts;

import static android.content.ContentProviderOperation.TYPE_DELETE;
import static android.content.ContentProviderOperation.TYPE_INSERT;
import static android.content.ContentProviderOperation.TYPE_UPDATE;

import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Intents.Insert;
import android.provider.ContactsContract.RawContacts;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest;

import com.android.contacts.common.model.AccountTypeManager;
import com.android.contacts.model.RawContact;
import com.android.contacts.model.RawContactDelta;
import com.android.contacts.common.model.ValuesDelta;
import com.android.contacts.model.RawContactDeltaList;
import com.android.contacts.model.RawContactModifier;
import com.android.contacts.common.model.account.AccountType;
import com.android.contacts.common.model.account.AccountType.EditType;
import com.android.contacts.common.model.account.ExchangeAccountType;
import com.android.contacts.common.model.account.GoogleAccountType;
import com.android.contacts.common.model.dataitem.DataKind;
import com.android.contacts.common.test.mocks.ContactsMockContext;
import com.android.contacts.tests.mocks.MockAccountTypeManager;
import com.android.contacts.common.test.mocks.MockContentProvider;
import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.List;

/**
 * Tests for {@link RawContactModifier} to verify that {@link AccountType}
 * constraints are being enforced correctly.
 */
@LargeTest
public class RawContactModifierTests extends AndroidTestCase {
    public static final String TAG = "EntityModifierTests";

    public static final long VER_FIRST = 100;

    private static final long TEST_ID = 4;
    private static final String TEST_PHONE = "218-555-1212";
    private static final String TEST_NAME = "Adam Young";
    private static final String TEST_NAME2 = "Breanne Duren";
    private static final String TEST_IM = "example@example.com";
    private static final String TEST_POSTAL = "1600 Amphitheatre Parkway";

    private static final String TEST_ACCOUNT_NAME = "unittest@example.com";
    private static final String TEST_ACCOUNT_TYPE = "com.example.unittest";

    private static final String EXCHANGE_ACCT_TYPE = "com.android.exchange";

    @Override
    public void setUp() {
        mContext = getContext();
    }

    public static class MockContactsSource extends AccountType {

        MockContactsSource() {
            try {
                this.accountType = TEST_ACCOUNT_TYPE;

                final DataKind nameKind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
                        R.string.nameLabelsGroup, -1, true);
                nameKind.typeOverallMax = 1;
                addKind(nameKind);

                // Phone allows maximum 2 home, 1 work, and unlimited other, with
                // constraint of 5 numbers maximum.
                final DataKind phoneKind = new DataKind(
                        Phone.CONTENT_ITEM_TYPE, -1, 10, true);

                phoneKind.typeOverallMax = 5;
                phoneKind.typeColumn = Phone.TYPE;
                phoneKind.typeList = Lists.newArrayList();
                phoneKind.typeList.add(new EditType(Phone.TYPE_HOME, -1).setSpecificMax(2));
                phoneKind.typeList.add(new EditType(Phone.TYPE_WORK, -1).setSpecificMax(1));
                phoneKind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, -1).setSecondary(true));
                phoneKind.typeList.add(new EditType(Phone.TYPE_OTHER, -1));

                phoneKind.fieldList = Lists.newArrayList();
                phoneKind.fieldList.add(new EditField(Phone.NUMBER, -1, -1));
                phoneKind.fieldList.add(new EditField(Phone.LABEL, -1, -1));

                addKind(phoneKind);

                // Email is unlimited
                final DataKind emailKind = new DataKind(Email.CONTENT_ITEM_TYPE, -1, 10, true);
                emailKind.typeOverallMax = -1;
                emailKind.fieldList = Lists.newArrayList();
                emailKind.fieldList.add(new EditField(Email.DATA, -1, -1));
                addKind(emailKind);

                // IM is only one
                final DataKind imKind = new DataKind(Im.CONTENT_ITEM_TYPE, -1, 10, true);
                imKind.typeOverallMax = 1;
                imKind.fieldList = Lists.newArrayList();
                imKind.fieldList.add(new EditField(Im.DATA, -1, -1));
                addKind(imKind);

                // Organization is only one
                final DataKind orgKind = new DataKind(Organization.CONTENT_ITEM_TYPE, -1, 10, true);
                orgKind.typeOverallMax = 1;
                orgKind.fieldList = Lists.newArrayList();
                orgKind.fieldList.add(new EditField(Organization.COMPANY, -1, -1));
                orgKind.fieldList.add(new EditField(Organization.TITLE, -1, -1));
                addKind(orgKind);
            } catch (DefinitionException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public boolean isGroupMembershipEditable() {
            return false;
        }

        @Override
        public boolean areContactsWritable() {
            return true;
        }
    }

    /**
     * Build a {@link AccountType} that has various odd constraints for
     * testing purposes.
     */
    protected AccountType getAccountType() {
        return new MockContactsSource();
    }

    /**
     * Build {@link AccountTypeManager} instance.
     */
    protected AccountTypeManager getAccountTypes(AccountType... types) {
        return new MockAccountTypeManager(types, null);
    }

    /**
     * Build an {@link RawContact} with the requested set of phone numbers.
     */
    protected RawContactDelta getRawContact(Long existingId, ContentValues... entries) {
        final ContentValues contact = new ContentValues();
        if (existingId != null) {
            contact.put(RawContacts._ID, existingId);
        }
        contact.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
        contact.put(RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE);

        final RawContact before = new RawContact(contact);
        for (ContentValues values : entries) {
            before.addDataItemValues(values);
        }
        return RawContactDelta.fromBefore(before);
    }

    /**
     * Assert this {@link List} contains the given {@link Object}.
     */
    protected void assertContains(List<?> list, Object object) {
        assertTrue("Missing expected value", list.contains(object));
    }

    /**
     * Assert this {@link List} does not contain the given {@link Object}.
     */
    protected void assertNotContains(List<?> list, Object object) {
        assertFalse("Contained unexpected value", list.contains(object));
    }

    /**
     * Insert various rows to test
     * {@link RawContactModifier#getValidTypes(RawContactDelta, DataKind, EditType)}
     */
    public void testValidTypes() {
        // Build a source and pull specific types
        final AccountType source = getAccountType();
        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
        final EditType typeWork = RawContactModifier.getType(kindPhone, Phone.TYPE_WORK);
        final EditType typeOther = RawContactModifier.getType(kindPhone, Phone.TYPE_OTHER);

        List<EditType> validTypes;

        // Add first home, first work
        final RawContactDelta state = getRawContact(TEST_ID);
        RawContactModifier.insertChild(state, kindPhone, typeHome);
        RawContactModifier.insertChild(state, kindPhone, typeWork);

        // Expecting home, other
        validTypes = RawContactModifier.getValidTypes(state, kindPhone, null);
        assertContains(validTypes, typeHome);
        assertNotContains(validTypes, typeWork);
        assertContains(validTypes, typeOther);

        // Add second home
        RawContactModifier.insertChild(state, kindPhone, typeHome);

        // Expecting other
        validTypes = RawContactModifier.getValidTypes(state, kindPhone, null);
        assertNotContains(validTypes, typeHome);
        assertNotContains(validTypes, typeWork);
        assertContains(validTypes, typeOther);

        // Add third and fourth home (invalid, but possible)
        RawContactModifier.insertChild(state, kindPhone, typeHome);
        RawContactModifier.insertChild(state, kindPhone, typeHome);

        // Expecting none
        validTypes = RawContactModifier.getValidTypes(state, kindPhone, null);
        assertNotContains(validTypes, typeHome);
        assertNotContains(validTypes, typeWork);
        assertNotContains(validTypes, typeOther);
    }

    /**
     * Test {@link RawContactModifier#canInsert(RawContactDelta, DataKind)} by
     * inserting various rows.
     */
    public void testCanInsert() {
        // Build a source and pull specific types
        final AccountType source = getAccountType();
        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
        final EditType typeWork = RawContactModifier.getType(kindPhone, Phone.TYPE_WORK);
        final EditType typeOther = RawContactModifier.getType(kindPhone, Phone.TYPE_OTHER);

        // Add first home, first work
        final RawContactDelta state = getRawContact(TEST_ID);
        RawContactModifier.insertChild(state, kindPhone, typeHome);
        RawContactModifier.insertChild(state, kindPhone, typeWork);
        assertTrue("Unable to insert", RawContactModifier.canInsert(state, kindPhone));

        // Add two other, which puts us just under "5" overall limit
        RawContactModifier.insertChild(state, kindPhone, typeOther);
        RawContactModifier.insertChild(state, kindPhone, typeOther);
        assertTrue("Unable to insert", RawContactModifier.canInsert(state, kindPhone));

        // Add second home, which should push to snug limit
        RawContactModifier.insertChild(state, kindPhone, typeHome);
        assertFalse("Able to insert", RawContactModifier.canInsert(state, kindPhone));
    }

    /**
     * Test
     * {@link RawContactModifier#getBestValidType(RawContactDelta, DataKind, boolean, int)}
     * by asserting expected best options in various states.
     */
    public void testBestValidType() {
        // Build a source and pull specific types
        final AccountType source = getAccountType();
        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
        final EditType typeWork = RawContactModifier.getType(kindPhone, Phone.TYPE_WORK);
        final EditType typeFaxWork = RawContactModifier.getType(kindPhone, Phone.TYPE_FAX_WORK);
        final EditType typeOther = RawContactModifier.getType(kindPhone, Phone.TYPE_OTHER);

        EditType suggested;

        // Default suggestion should be home
        final RawContactDelta state = getRawContact(TEST_ID);
        suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
        assertEquals("Unexpected suggestion", typeHome, suggested);

        // Add first home, should now suggest work
        RawContactModifier.insertChild(state, kindPhone, typeHome);
        suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
        assertEquals("Unexpected suggestion", typeWork, suggested);

        // Add work fax, should still suggest work
        RawContactModifier.insertChild(state, kindPhone, typeFaxWork);
        suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
        assertEquals("Unexpected suggestion", typeWork, suggested);

        // Add other, should still suggest work
        RawContactModifier.insertChild(state, kindPhone, typeOther);
        suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
        assertEquals("Unexpected suggestion", typeWork, suggested);

        // Add work, now should suggest other
        RawContactModifier.insertChild(state, kindPhone, typeWork);
        suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
        assertEquals("Unexpected suggestion", typeOther, suggested);
    }

    public void testIsEmptyEmpty() {
        final AccountType source = getAccountType();
        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);

        // Test entirely empty row
        final ContentValues after = new ContentValues();
        final ValuesDelta values = ValuesDelta.fromAfter(after);

        assertTrue("Expected empty", RawContactModifier.isEmpty(values, kindPhone));
    }

    public void testIsEmptyDirectFields() {
        final AccountType source = getAccountType();
        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);

        // Test row that has type values, but core fields are empty
        final RawContactDelta state = getRawContact(TEST_ID);
        final ValuesDelta values = RawContactModifier.insertChild(state, kindPhone, typeHome);

        assertTrue("Expected empty", RawContactModifier.isEmpty(values, kindPhone));

        // Insert some data to trigger non-empty state
        values.put(Phone.NUMBER, TEST_PHONE);

        assertFalse("Expected non-empty", RawContactModifier.isEmpty(values, kindPhone));
    }

    public void testTrimEmptySingle() {
        final AccountType source = getAccountType();
        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);

        // Test row that has type values, but core fields are empty
        final RawContactDelta state = getRawContact(TEST_ID);
        RawContactModifier.insertChild(state, kindPhone, typeHome);

        // Build diff, expecting insert for data row and update enforcement
        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 3, diff.size());
        {
            final ContentProviderOperation oper = diff.get(0);
            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(1);
            assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(2);
            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }

        // Trim empty rows and try again, expecting delete of overall contact
        RawContactModifier.trimEmpty(state, source);
        diff.clear();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 1, diff.size());
        {
            final ContentProviderOperation oper = diff.get(0);
            assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }
    }

    public void testTrimEmptySpaces() {
        final AccountType source = getAccountType();
        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);

        // Test row that has type values, but values are spaces
        final RawContactDelta state = RawContactDeltaListTests.buildBeforeEntity(mContext, TEST_ID,
                VER_FIRST);
        final ValuesDelta values = RawContactModifier.insertChild(state, kindPhone, typeHome);
        values.put(Phone.NUMBER, "   ");

        // Build diff, expecting insert for data row and update enforcement
        RawContactDeltaListTests.assertDiffPattern(state,
                RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
                RawContactDeltaListTests.buildUpdateAggregationSuspended(),
                RawContactDeltaListTests.buildOper(Data.CONTENT_URI, TYPE_INSERT,
                        RawContactDeltaListTests.buildDataInsert(values, TEST_ID)),
                RawContactDeltaListTests.buildUpdateAggregationDefault());

        // Trim empty rows and try again, expecting delete of overall contact
        RawContactModifier.trimEmpty(state, source);
        RawContactDeltaListTests.assertDiffPattern(state,
                RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
                RawContactDeltaListTests.buildDelete(RawContacts.CONTENT_URI));
    }

    public void testTrimLeaveValid() {
        final AccountType source = getAccountType();
        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);

        // Test row that has type values with valid number
        final RawContactDelta state = RawContactDeltaListTests.buildBeforeEntity(mContext, TEST_ID,
                VER_FIRST);
        final ValuesDelta values = RawContactModifier.insertChild(state, kindPhone, typeHome);
        values.put(Phone.NUMBER, TEST_PHONE);

        // Build diff, expecting insert for data row and update enforcement
        RawContactDeltaListTests.assertDiffPattern(state,
                RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
                RawContactDeltaListTests.buildUpdateAggregationSuspended(),
                RawContactDeltaListTests.buildOper(Data.CONTENT_URI, TYPE_INSERT,
                        RawContactDeltaListTests.buildDataInsert(values, TEST_ID)),
                RawContactDeltaListTests.buildUpdateAggregationDefault());

        // Trim empty rows and try again, expecting no differences
        RawContactModifier.trimEmpty(state, source);
        RawContactDeltaListTests.assertDiffPattern(state,
                RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
                RawContactDeltaListTests.buildUpdateAggregationSuspended(),
                RawContactDeltaListTests.buildOper(Data.CONTENT_URI, TYPE_INSERT,
                        RawContactDeltaListTests.buildDataInsert(values, TEST_ID)),
                RawContactDeltaListTests.buildUpdateAggregationDefault());
    }

    public void testTrimEmptyUntouched() {
        final AccountType source = getAccountType();
        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);

        // Build "before" that has empty row
        final RawContactDelta state = getRawContact(TEST_ID);
        final ContentValues before = new ContentValues();
        before.put(Data._ID, TEST_ID);
        before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
        state.addEntry(ValuesDelta.fromBefore(before));

        // Build diff, expecting no changes
        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 0, diff.size());

        // Try trimming existing empty, which we shouldn't touch
        RawContactModifier.trimEmpty(state, source);
        diff.clear();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 0, diff.size());
    }

    public void testTrimEmptyAfterUpdate() {
        final AccountType source = getAccountType();
        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);

        // Build "before" that has row with some phone number
        final ContentValues before = new ContentValues();
        before.put(Data._ID, TEST_ID);
        before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
        before.put(kindPhone.typeColumn, typeHome.rawValue);
        before.put(Phone.NUMBER, TEST_PHONE);
        final RawContactDelta state = getRawContact(TEST_ID, before);

        // Build diff, expecting no changes
        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 0, diff.size());

        // Now update row by changing number to empty string, expecting single update
        final ValuesDelta child = state.getEntry(TEST_ID);
        child.put(Phone.NUMBER, "");
        diff.clear();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 3, diff.size());
        {
            final ContentProviderOperation oper = diff.get(0);
            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(1);
            assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(2);
            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }

        // Now run trim, which should turn that update into delete
        RawContactModifier.trimEmpty(state, source);
        diff.clear();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 1, diff.size());
        {
            final ContentProviderOperation oper = diff.get(0);
            assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }
    }

    public void testTrimInsertEmpty() {
        final AccountType accountType = getAccountType();
        final AccountTypeManager accountTypes = getAccountTypes(accountType);
        final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);

        // Try creating a contact without any child entries
        final RawContactDelta state = getRawContact(null);
        final RawContactDeltaList set = new RawContactDeltaList();
        set.add(state);


        // Build diff, expecting single insert
        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 2, diff.size());
        {
            final ContentProviderOperation oper = diff.get(0);
            assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }

        // Trim empty rows and try again, expecting no insert
        RawContactModifier.trimEmpty(set, accountTypes);
        diff.clear();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 0, diff.size());
    }

    public void testTrimInsertInsert() {
        final AccountType accountType = getAccountType();
        final AccountTypeManager accountTypes = getAccountTypes(accountType);
        final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);

        // Try creating a contact with single empty entry
        final RawContactDelta state = getRawContact(null);
        RawContactModifier.insertChild(state, kindPhone, typeHome);
        final RawContactDeltaList set = new RawContactDeltaList();
        set.add(state);

        // Build diff, expecting two insert operations
        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 3, diff.size());
        {
            final ContentProviderOperation oper = diff.get(0);
            assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(1);
            assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
        }

        // Trim empty rows and try again, expecting silence
        RawContactModifier.trimEmpty(set, accountTypes);
        diff.clear();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 0, diff.size());
    }

    public void testTrimUpdateRemain() {
        final AccountType accountType = getAccountType();
        final AccountTypeManager accountTypes = getAccountTypes(accountType);
        final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);

        // Build "before" with two phone numbers
        final ContentValues first = new ContentValues();
        first.put(Data._ID, TEST_ID);
        first.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
        first.put(kindPhone.typeColumn, typeHome.rawValue);
        first.put(Phone.NUMBER, TEST_PHONE);

        final ContentValues second = new ContentValues();
        second.put(Data._ID, TEST_ID);
        second.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
        second.put(kindPhone.typeColumn, typeHome.rawValue);
        second.put(Phone.NUMBER, TEST_PHONE);

        final RawContactDelta state = getRawContact(TEST_ID, first, second);
        final RawContactDeltaList set = new RawContactDeltaList();
        set.add(state);

        // Build diff, expecting no changes
        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 0, diff.size());

        // Now update row by changing number to empty string, expecting single update
        final ValuesDelta child = state.getEntry(TEST_ID);
        child.put(Phone.NUMBER, "");
        diff.clear();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 3, diff.size());
        {
            final ContentProviderOperation oper = diff.get(0);
            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(1);
            assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(2);
            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }

        // Now run trim, which should turn that update into delete
        RawContactModifier.trimEmpty(set, accountTypes);
        diff.clear();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 3, diff.size());
        {
            final ContentProviderOperation oper = diff.get(0);
            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(1);
            assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(2);
            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }
    }

    public void testTrimUpdateUpdate() {
        final AccountType accountType = getAccountType();
        final AccountTypeManager accountTypes = getAccountTypes(accountType);
        final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);

        // Build "before" with two phone numbers
        final ContentValues first = new ContentValues();
        first.put(Data._ID, TEST_ID);
        first.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
        first.put(kindPhone.typeColumn, typeHome.rawValue);
        first.put(Phone.NUMBER, TEST_PHONE);

        final RawContactDelta state = getRawContact(TEST_ID, first);
        final RawContactDeltaList set = new RawContactDeltaList();
        set.add(state);

        // Build diff, expecting no changes
        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 0, diff.size());

        // Now update row by changing number to empty string, expecting single update
        final ValuesDelta child = state.getEntry(TEST_ID);
        child.put(Phone.NUMBER, "");
        diff.clear();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 3, diff.size());
        {
            final ContentProviderOperation oper = diff.get(0);
            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(1);
            assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
        }
        {
            final ContentProviderOperation oper = diff.get(2);
            assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }

        // Now run trim, which should turn into deleting the whole contact
        RawContactModifier.trimEmpty(set, accountTypes);
        diff.clear();
        state.buildDiff(diff);
        assertEquals("Unexpected operations", 1, diff.size());
        {
            final ContentProviderOperation oper = diff.get(0);
            assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
        }
    }

    public void testParseExtrasExistingName() {
        final AccountType accountType = getAccountType();

        // Build "before" name
        final ContentValues first = new ContentValues();
        first.put(Data._ID, TEST_ID);
        first.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
        first.put(StructuredName.GIVEN_NAME, TEST_NAME);

        // Parse extras, making sure we keep single name
        final RawContactDelta state = getRawContact(TEST_ID, first);
        final Bundle extras = new Bundle();
        extras.putString(Insert.NAME, TEST_NAME2);
        RawContactModifier.parseExtras(mContext, accountType, state, extras);

        final int nameCount = state.getMimeEntriesCount(StructuredName.CONTENT_ITEM_TYPE, true);
        assertEquals("Unexpected names", 1, nameCount);
    }

    public void testParseExtrasIgnoreLimit() {
        final AccountType accountType = getAccountType();

        // Build "before" IM
        final ContentValues first = new ContentValues();
        first.put(Data._ID, TEST_ID);
        first.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
        first.put(Im.DATA, TEST_IM);

        final RawContactDelta state = getRawContact(TEST_ID, first);
        final int beforeCount = state.getMimeEntries(Im.CONTENT_ITEM_TYPE).size();

        // We should ignore data that doesn't fit account type rules, since account type
        // only allows single Im
        final Bundle extras = new Bundle();
        extras.putInt(Insert.IM_PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
        extras.putString(Insert.IM_HANDLE, TEST_IM);
        RawContactModifier.parseExtras(mContext, accountType, state, extras);

        final int afterCount = state.getMimeEntries(Im.CONTENT_ITEM_TYPE).size();
        assertEquals("Broke account type rules", beforeCount, afterCount);
    }

    public void testParseExtrasIgnoreUnhandled() {
        final AccountType accountType = getAccountType();
        final RawContactDelta state = getRawContact(TEST_ID);

        // We should silently ignore types unsupported by account type
        final Bundle extras = new Bundle();
        extras.putString(Insert.POSTAL, TEST_POSTAL);
        RawContactModifier.parseExtras(mContext, accountType, state, extras);

        assertNull("Broke accoun type rules",
                state.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE));
    }

    public void testParseExtrasJobTitle() {
        final AccountType accountType = getAccountType();
        final RawContactDelta state = getRawContact(TEST_ID);

        // Make sure that we create partial Organizations
        final Bundle extras = new Bundle();
        extras.putString(Insert.JOB_TITLE, TEST_NAME);
        RawContactModifier.parseExtras(mContext, accountType, state, extras);

        final int count = state.getMimeEntries(Organization.CONTENT_ITEM_TYPE).size();
        assertEquals("Expected to create organization", 1, count);
    }

    public void testMigrateWithDisplayNameFromGoogleToExchange1() {
        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
        DataKind kind = newAccountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);

        ContactsMockContext context = new ContactsMockContext(getContext());

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
        mockNameValues.put(StructuredName.PREFIX, "prefix");
        mockNameValues.put(StructuredName.GIVEN_NAME, "given");
        mockNameValues.put(StructuredName.MIDDLE_NAME, "middle");
        mockNameValues.put(StructuredName.FAMILY_NAME, "family");
        mockNameValues.put(StructuredName.SUFFIX, "suffix");
        mockNameValues.put(StructuredName.PHONETIC_FAMILY_NAME, "PHONETIC_FAMILY");
        mockNameValues.put(StructuredName.PHONETIC_MIDDLE_NAME, "PHONETIC_MIDDLE");
        mockNameValues.put(StructuredName.PHONETIC_GIVEN_NAME, "PHONETIC_GIVEN");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migrateStructuredName(context, oldState, newState, kind);
        List<ValuesDelta> list = newState.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
        assertEquals(1, list.size());

        ContentValues output = list.get(0).getAfter();
        assertEquals("prefix", output.getAsString(StructuredName.PREFIX));
        assertEquals("given", output.getAsString(StructuredName.GIVEN_NAME));
        assertEquals("middle", output.getAsString(StructuredName.MIDDLE_NAME));
        assertEquals("family", output.getAsString(StructuredName.FAMILY_NAME));
        assertEquals("suffix", output.getAsString(StructuredName.SUFFIX));
        // Phonetic middle name isn't supported by Exchange.
        assertEquals("PHONETIC_FAMILY", output.getAsString(StructuredName.PHONETIC_FAMILY_NAME));
        assertEquals("PHONETIC_GIVEN", output.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
    }

    public void testMigrateWithDisplayNameFromGoogleToExchange2() {
        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
        DataKind kind = newAccountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);

        ContactsMockContext context = new ContactsMockContext(getContext());
        MockContentProvider provider = context.getContactsProvider();

        String inputDisplayName = "prefix given middle family suffix";
        // The method will ask the provider to split/join StructuredName.
        Uri uriForBuildDisplayName =
                ContactsContract.AUTHORITY_URI
                        .buildUpon()
                        .appendPath("complete_name")
                        .appendQueryParameter(StructuredName.DISPLAY_NAME, inputDisplayName)
                        .build();
        provider.expectQuery(uriForBuildDisplayName)
                .returnRow("prefix", "given", "middle", "family", "suffix")
                .withProjection(StructuredName.PREFIX, StructuredName.GIVEN_NAME,
                        StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME,
                        StructuredName.SUFFIX);

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
        mockNameValues.put(StructuredName.DISPLAY_NAME, inputDisplayName);
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migrateStructuredName(context, oldState, newState, kind);
        List<ValuesDelta> list = newState.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
        assertEquals(1, list.size());

        ContentValues outputValues = list.get(0).getAfter();
        assertEquals("prefix", outputValues.getAsString(StructuredName.PREFIX));
        assertEquals("given", outputValues.getAsString(StructuredName.GIVEN_NAME));
        assertEquals("middle", outputValues.getAsString(StructuredName.MIDDLE_NAME));
        assertEquals("family", outputValues.getAsString(StructuredName.FAMILY_NAME));
        assertEquals("suffix", outputValues.getAsString(StructuredName.SUFFIX));
    }

    public void testMigrateWithStructuredNameFromExchangeToGoogle() {
        AccountType oldAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
        AccountType newAccountType = new GoogleAccountType(getContext(), "");
        DataKind kind = newAccountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);

        ContactsMockContext context = new ContactsMockContext(getContext());
        MockContentProvider provider = context.getContactsProvider();

        // The method will ask the provider to split/join StructuredName.
        Uri uriForBuildDisplayName =
                ContactsContract.AUTHORITY_URI
                        .buildUpon()
                        .appendPath("complete_name")
                        .appendQueryParameter(StructuredName.PREFIX, "prefix")
                        .appendQueryParameter(StructuredName.GIVEN_NAME, "given")
                        .appendQueryParameter(StructuredName.MIDDLE_NAME, "middle")
                        .appendQueryParameter(StructuredName.FAMILY_NAME, "family")
                        .appendQueryParameter(StructuredName.SUFFIX, "suffix")
                        .build();
        provider.expectQuery(uriForBuildDisplayName)
                .returnRow("prefix given middle family suffix")
                .withProjection(StructuredName.DISPLAY_NAME);

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
        mockNameValues.put(StructuredName.PREFIX, "prefix");
        mockNameValues.put(StructuredName.GIVEN_NAME, "given");
        mockNameValues.put(StructuredName.MIDDLE_NAME, "middle");
        mockNameValues.put(StructuredName.FAMILY_NAME, "family");
        mockNameValues.put(StructuredName.SUFFIX, "suffix");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migrateStructuredName(context, oldState, newState, kind);

        List<ValuesDelta> list = newState.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
        assertNotNull(list);
        assertEquals(1, list.size());
        ContentValues outputValues = list.get(0).getAfter();
        assertEquals("prefix given middle family suffix",
                outputValues.getAsString(StructuredName.DISPLAY_NAME));
    }

    public void testMigratePostalFromGoogleToExchange() {
        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
        DataKind kind = newAccountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
        mockNameValues.put(StructuredPostal.FORMATTED_ADDRESS, "formatted_address");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migratePostal(oldState, newState, kind);

        List<ValuesDelta> list = newState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE);
        assertNotNull(list);
        assertEquals(1, list.size());
        ContentValues outputValues = list.get(0).getAfter();
        // FORMATTED_ADDRESS isn't supported by Exchange.
        assertNull(outputValues.getAsString(StructuredPostal.FORMATTED_ADDRESS));
        assertEquals("formatted_address", outputValues.getAsString(StructuredPostal.STREET));
    }

    public void testMigratePostalFromExchangeToGoogle() {
        AccountType oldAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
        AccountType newAccountType = new GoogleAccountType(getContext(), "");
        DataKind kind = newAccountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
        mockNameValues.put(StructuredPostal.COUNTRY, "country");
        mockNameValues.put(StructuredPostal.POSTCODE, "postcode");
        mockNameValues.put(StructuredPostal.REGION, "region");
        mockNameValues.put(StructuredPostal.CITY, "city");
        mockNameValues.put(StructuredPostal.STREET, "street");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migratePostal(oldState, newState, kind);

        List<ValuesDelta> list = newState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE);
        assertNotNull(list);
        assertEquals(1, list.size());
        ContentValues outputValues = list.get(0).getAfter();

        // Check FORMATTED_ADDRESS contains all info.
        String formattedAddress = outputValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
        assertNotNull(formattedAddress);
        assertTrue(formattedAddress.contains("country"));
        assertTrue(formattedAddress.contains("postcode"));
        assertTrue(formattedAddress.contains("region"));
        assertTrue(formattedAddress.contains("postcode"));
        assertTrue(formattedAddress.contains("city"));
        assertTrue(formattedAddress.contains("street"));
    }

    public void testMigrateEventFromGoogleToExchange1() {
        testMigrateEventCommon(new GoogleAccountType(getContext(), ""),
                new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE));
    }

    public void testMigrateEventFromExchangeToGoogle() {
        testMigrateEventCommon(new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE),
                new GoogleAccountType(getContext(), ""));
    }

    private void testMigrateEventCommon(AccountType oldAccountType, AccountType newAccountType) {
        DataKind kind = newAccountType.getKindForMimetype(Event.CONTENT_ITEM_TYPE);

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
        mockNameValues.put(Event.START_DATE, "1972-02-08");
        mockNameValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migrateEvent(oldState, newState, kind, 1990);

        List<ValuesDelta> list = newState.getMimeEntries(Event.CONTENT_ITEM_TYPE);
        assertNotNull(list);
        assertEquals(1, list.size());  // Anniversary should be dropped.
        ContentValues outputValues = list.get(0).getAfter();

        assertEquals("1972-02-08", outputValues.getAsString(Event.START_DATE));
        assertEquals(Event.TYPE_BIRTHDAY, outputValues.getAsInteger(Event.TYPE).intValue());
    }

    public void testMigrateEventFromGoogleToExchange2() {
        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
        DataKind kind = newAccountType.getKindForMimetype(Event.CONTENT_ITEM_TYPE);

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
        // No year format is not supported by Exchange.
        mockNameValues.put(Event.START_DATE, "--06-01");
        mockNameValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
        mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
        mockNameValues.put(Event.START_DATE, "1980-08-02");
        // Anniversary is not supported by Exchange
        mockNameValues.put(Event.TYPE, Event.TYPE_ANNIVERSARY);
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migrateEvent(oldState, newState, kind, 1990);

        List<ValuesDelta> list = newState.getMimeEntries(Event.CONTENT_ITEM_TYPE);
        assertNotNull(list);
        assertEquals(1, list.size());  // Anniversary should be dropped.
        ContentValues outputValues = list.get(0).getAfter();

        // Default year should be used.
        assertEquals("1990-06-01", outputValues.getAsString(Event.START_DATE));
        assertEquals(Event.TYPE_BIRTHDAY, outputValues.getAsInteger(Event.TYPE).intValue());
    }

    public void testMigrateEmailFromGoogleToExchange() {
        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
        DataKind kind = newAccountType.getKindForMimetype(Email.CONTENT_ITEM_TYPE);

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
        mockNameValues.put(Email.TYPE, Email.TYPE_CUSTOM);
        mockNameValues.put(Email.LABEL, "custom_type");
        mockNameValues.put(Email.ADDRESS, "address1");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
        mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
        mockNameValues.put(Email.TYPE, Email.TYPE_HOME);
        mockNameValues.put(Email.ADDRESS, "address2");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
        mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
        mockNameValues.put(Email.TYPE, Email.TYPE_WORK);
        mockNameValues.put(Email.ADDRESS, "address3");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
        // Exchange can have up to 3 email entries. This 4th entry should be dropped.
        mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
        mockNameValues.put(Email.TYPE, Email.TYPE_OTHER);
        mockNameValues.put(Email.ADDRESS, "address4");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migrateGenericWithTypeColumn(oldState, newState, kind);

        List<ValuesDelta> list = newState.getMimeEntries(Email.CONTENT_ITEM_TYPE);
        assertNotNull(list);
        assertEquals(3, list.size());

        ContentValues outputValues = list.get(0).getAfter();
        assertEquals(Email.TYPE_CUSTOM, outputValues.getAsInteger(Email.TYPE).intValue());
        assertEquals("custom_type", outputValues.getAsString(Email.LABEL));
        assertEquals("address1", outputValues.getAsString(Email.ADDRESS));

        outputValues = list.get(1).getAfter();
        assertEquals(Email.TYPE_HOME, outputValues.getAsInteger(Email.TYPE).intValue());
        assertEquals("address2", outputValues.getAsString(Email.ADDRESS));

        outputValues = list.get(2).getAfter();
        assertEquals(Email.TYPE_WORK, outputValues.getAsInteger(Email.TYPE).intValue());
        assertEquals("address3", outputValues.getAsString(Email.ADDRESS));
    }

    public void testMigrateImFromGoogleToExchange() {
        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
        DataKind kind = newAccountType.getKindForMimetype(Im.CONTENT_ITEM_TYPE);

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
        // Exchange doesn't support TYPE_HOME
        mockNameValues.put(Im.TYPE, Im.TYPE_HOME);
        mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_JABBER);
        mockNameValues.put(Im.DATA, "im1");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
        // Exchange doesn't support TYPE_WORK
        mockNameValues.put(Im.TYPE, Im.TYPE_WORK);
        mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_YAHOO);
        mockNameValues.put(Im.DATA, "im2");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
        mockNameValues.put(Im.TYPE, Im.TYPE_OTHER);
        mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM);
        mockNameValues.put(Im.CUSTOM_PROTOCOL, "custom_protocol");
        mockNameValues.put(Im.DATA, "im3");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        // Exchange can have up to 3 IM entries. This 4th entry should be dropped.
        mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
        mockNameValues.put(Im.TYPE, Im.TYPE_OTHER);
        mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
        mockNameValues.put(Im.DATA, "im4");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migrateGenericWithTypeColumn(oldState, newState, kind);

        List<ValuesDelta> list = newState.getMimeEntries(Im.CONTENT_ITEM_TYPE);
        assertNotNull(list);
        assertEquals(3, list.size());

        assertNotNull(kind.defaultValues.getAsInteger(Im.TYPE));

        int defaultType = kind.defaultValues.getAsInteger(Im.TYPE);

        ContentValues outputValues = list.get(0).getAfter();
        // HOME should become default type.
        assertEquals(defaultType, outputValues.getAsInteger(Im.TYPE).intValue());
        assertEquals(Im.PROTOCOL_JABBER, outputValues.getAsInteger(Im.PROTOCOL).intValue());
        assertEquals("im1", outputValues.getAsString(Im.DATA));

        outputValues = list.get(1).getAfter();
        assertEquals(defaultType, outputValues.getAsInteger(Im.TYPE).intValue());
        assertEquals(Im.PROTOCOL_YAHOO, outputValues.getAsInteger(Im.PROTOCOL).intValue());
        assertEquals("im2", outputValues.getAsString(Im.DATA));

        outputValues = list.get(2).getAfter();
        assertEquals(defaultType, outputValues.getAsInteger(Im.TYPE).intValue());
        assertEquals(Im.PROTOCOL_CUSTOM, outputValues.getAsInteger(Im.PROTOCOL).intValue());
        assertEquals("custom_protocol", outputValues.getAsString(Im.CUSTOM_PROTOCOL));
        assertEquals("im3", outputValues.getAsString(Im.DATA));
    }

    public void testMigratePhoneFromGoogleToExchange() {
        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
        DataKind kind = newAccountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);

        // Create 5 numbers.
        // - "1" -- HOME
        // - "2" -- WORK
        // - "3" -- CUSTOM
        // - "4" -- WORK
        // - "5" -- WORK_MOBILE
        // Then we convert it to Exchange account type.
        // - "1" -- HOME
        // - "2" -- WORK
        // - "3" -- Because CUSTOM is not supported, it'll be changed to the default, MOBILE
        // - "4" -- WORK
        // - "5" -- WORK_MOBILE not suppoted again, so will be MOBILE.
        // But then, Exchange doesn't support multiple MOBILE numbers, so "5" will be removed.
        // i.e. the result will be:
        // - "1" -- HOME
        // - "2" -- WORK
        // - "3" -- MOBILE
        // - "4" -- WORK

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
        mockNameValues.put(Phone.TYPE, Phone.TYPE_HOME);
        mockNameValues.put(Phone.NUMBER, "1");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
        mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
        mockNameValues.put(Phone.TYPE, Phone.TYPE_WORK);
        mockNameValues.put(Phone.NUMBER, "2");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
        mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
        // Exchange doesn't support this type. Default to MOBILE
        mockNameValues.put(Phone.TYPE, Phone.TYPE_CUSTOM);
        mockNameValues.put(Phone.LABEL, "custom_type");
        mockNameValues.put(Phone.NUMBER, "3");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
        mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
        mockNameValues.put(Phone.TYPE, Phone.TYPE_WORK);
        mockNameValues.put(Phone.NUMBER, "4");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
        mockNameValues = new ContentValues();

        mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
        mockNameValues.put(Phone.TYPE, Phone.TYPE_WORK_MOBILE);
        mockNameValues.put(Phone.NUMBER, "5");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migrateGenericWithTypeColumn(oldState, newState, kind);

        List<ValuesDelta> list = newState.getMimeEntries(Phone.CONTENT_ITEM_TYPE);
        assertNotNull(list);
        assertEquals(4, list.size());

        int defaultType = Phone.TYPE_MOBILE;

        ContentValues outputValues = list.get(0).getAfter();
        assertEquals(Phone.TYPE_HOME, outputValues.getAsInteger(Phone.TYPE).intValue());
        assertEquals("1", outputValues.getAsString(Phone.NUMBER));
        outputValues = list.get(1).getAfter();
        assertEquals(Phone.TYPE_WORK, outputValues.getAsInteger(Phone.TYPE).intValue());
        assertEquals("2", outputValues.getAsString(Phone.NUMBER));
        outputValues = list.get(2).getAfter();
        assertEquals(defaultType, outputValues.getAsInteger(Phone.TYPE).intValue());
        assertNull(outputValues.getAsInteger(Phone.LABEL));
        assertEquals("3", outputValues.getAsString(Phone.NUMBER));
        outputValues = list.get(3).getAfter();
        assertEquals(Phone.TYPE_WORK, outputValues.getAsInteger(Phone.TYPE).intValue());
        assertEquals("4", outputValues.getAsString(Phone.NUMBER));
    }

    public void testMigrateOrganizationFromGoogleToExchange() {
        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
        DataKind kind = newAccountType.getKindForMimetype(Organization.CONTENT_ITEM_TYPE);

        RawContactDelta oldState = new RawContactDelta();
        ContentValues mockNameValues = new ContentValues();
        mockNameValues.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
        mockNameValues.put(Organization.COMPANY, "company1");
        mockNameValues.put(Organization.DEPARTMENT, "department1");
        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));

        RawContactDelta newState = new RawContactDelta();
        RawContactModifier.migrateGenericWithoutTypeColumn(oldState, newState, kind);

        List<ValuesDelta> list = newState.getMimeEntries(Organization.CONTENT_ITEM_TYPE);
        assertNotNull(list);
        assertEquals(1, list.size());

        ContentValues outputValues = list.get(0).getAfter();
        assertEquals("company1", outputValues.getAsString(Organization.COMPANY));
        assertEquals("department1", outputValues.getAsString(Organization.DEPARTMENT));
    }
}
