/*
 * Copyright (C) 2010 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 android.provider.cts;

import com.google.android.collect.Lists;
import com.google.android.collect.Sets;

import android.content.ContentUris;
import android.content.ContentValues;
import android.content.IContentProvider;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.text.TextUtils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import java.util.Arrays;

import junit.framework.Assert;
import junit.framework.ComparisonFailure;

/**
 * A test data builder for ContactsContract tests.
 */
public class ContactsContract_TestDataBuilder {
    private IContentProvider mProvider;
    private ArrayList<Builder<?>> mCreatedRows = Lists.newArrayList();
    private HashSet<Builder<?>> mLoadedRows = Sets.newHashSet();

    private interface IdQuery {
        String[] COLUMNS = new String[] {
            BaseColumns._ID,
        };

        int _ID = 0;
    }

    public abstract class  Builder<T extends Builder<?>> extends Assert {
        protected ContentValues mValues = new ContentValues();
        private Uri mUri;
        private long mId = -1;
        private Cursor mCursor;

        protected abstract Uri getContentUri();

        public long getId() throws Exception {
            if (mId != -1) {
                return mId;
            }

            assertNotNull("Row has not be inserted or loaded yet", mUri);

            Cursor cursor = mProvider.query(mUri, IdQuery.COLUMNS, null, null, null);
            if (cursor != null) {
                try {
                    cursor.moveToFirst();
                    mId = cursor.getInt(IdQuery._ID);
                } finally {
                    cursor.close();
                }

            }
            assertTrue("Could not obtain _ID for URI: " + mUri, mId != -1);
            return mId;
        }

        @SuppressWarnings("unchecked")
        public T setUri(Uri uri) {
            mUri = uri;
            return (T)this;
        }

        public Uri getUri() {
            if (mUri == null) {
                assertTrue("Neither URI nor ID has been specified", mId != -1);
                mUri = ContentUris.withAppendedId(getContentUri(), mId);
            }
            return mUri;
        }

        @SuppressWarnings("unchecked")
        public T with(String key, String value) {
            mValues.put(key, value);
            return (T)this;
        }

        @SuppressWarnings("unchecked")
        public T with(String key, long value) {
            mValues.put(key, value);
            return (T)this;
        }

        @SuppressWarnings("unchecked")
        public T with(String key, byte[] value) {
            mValues.put(key, value);
            return (T)this;
        }

        @SuppressWarnings("unchecked")
        public T insert() throws Exception {
            insertDependent();
            mCreatedRows.add(this);
            return (T)this;
        }

        @SuppressWarnings("unchecked")
        public T insertDependent() throws Exception {
            mUri = mProvider.insert(getContentUri(), mValues);
            assertNotNull("Could not insert a row in " + getContentUri(), mUri);
            mId = ContentUris.parseId(mUri);
            return (T)this;
        }

        public void delete() throws Exception {
            int count = mProvider.delete(getUri(), null, null);
            assertEquals("Wrong number of rows deleted for " + getUri(), 1, count);
        }

        public void deletePermanently() throws Exception {
            Uri uri = getUri().buildUpon()
                    .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
                    .build();
            int count = mProvider.delete(uri, null, null);
            assertEquals("Wrong number of rows deleted for " + uri, 1, count);
            mCreatedRows.remove(this);
        }

        @SuppressWarnings("unchecked")
        public T load() throws Exception {
            close();
            mLoadedRows.add(this);

            mCursor = mProvider.query(getUri(), null, null, null, null);
            if (mCursor == null || !mCursor.moveToFirst()) {
                fail("No data rows for URI: " + getUri());
            }
            return (T)this;
        }

        @SuppressWarnings("unchecked")
        public T loadUsingValues() throws Exception {
            close();
            mLoadedRows.add(this);

            StringBuilder selection = new StringBuilder();
            ArrayList<String> selectionArgs = Lists.newArrayList();
            Set<Map.Entry<String, Object>> entries = mValues.valueSet();
            for (Map.Entry<String, Object> entry : entries) {
                String column = entry.getKey();
                Object value = entry.getValue();

                if (selection.length() != 0) {
                    selection.append(" AND ");
                }

                selection.append(column);
                if (value == null) {
                    selection.append(" NOT NULL");
                } else {
                    selection.append("=?");
                    selectionArgs.add(value.toString());
                }
            }
            mCursor = mProvider.query(getContentUri(), null,
                    selection.toString(),
                    selectionArgs.toArray(new String[0]), null);
            if (mCursor == null || !mCursor.moveToFirst()) {
                fail("No data rows for " + getContentUri() + "[" + mValues.toString() + "]");
            }
            mId = mCursor.getLong(getColumnIndex(BaseColumns._ID));
            return (T)this;
        }

        public long getLong(String columnName) {
            return mCursor.getLong(mCursor.getColumnIndex(columnName));
        }

        public void assertColumn(String columnName, long value) {
            assertEquals(value, mCursor.getLong(getColumnIndex(columnName)));
        }

        public void assertColumn(String columnName, String value) {
            assertEquals(value, mCursor.getString(getColumnIndex(columnName)));
        }

        public void assertColumn(String columnName, byte[] value) {
            assertEquals(value, mCursor.getBlob(getColumnIndex(columnName)));
        }

        public void assertEquals(byte[] expected, byte[] actual) {
            assertEquals(null, expected, actual);
        }

        public void assertEquals(String message, byte[] expected, byte[] actual) {
            if (expected == null && actual == null)
                return;
            if (expected != null && Arrays.equals(actual, expected))
                return;
            throw new ComparisonFailure(message, expected.toString(), actual.toString());
        }

        private int getColumnIndex(String columnName) {
            int index = mCursor.getColumnIndex(columnName);
            assertTrue("No such column: " + columnName +
                    ". Available columns: " + TextUtils.join(", ", mCursor.getColumnNames()),
                    index != -1);
            return index;
        }

        public void close() {
            if (mCursor != null) {
                mCursor.close();
                mCursor = null;
            }
            mLoadedRows.remove(this);
        }

    }

    public class TestRawContact extends Builder<TestRawContact> {

        private TestContact mContact;

        @Override
        protected Uri getContentUri() {
            return RawContacts.CONTENT_URI;
        }

        public TestData newDataRow(String mimeType) {
            return new TestData(this, mimeType);
        }

        public long getContactId() {
            return getLong(RawContacts.CONTACT_ID);
        }

        public TestContact getContact() throws Exception {
            if (mContact == null) {
                mContact = new NestedTestContact(this);
            }
            return mContact;
        }
    }

    public class TestContact extends Builder<TestContact> {

        @Override
        protected Uri getContentUri() {
            return Contacts.CONTENT_URI;
        }
    }

    private class NestedTestContact extends TestContact {
        private final TestRawContact mRawContact;

        public NestedTestContact(TestRawContact rawContact) {
            mRawContact = rawContact;
        }

        @Override
        public long getId() throws Exception {
            return mRawContact.getContactId();
        }

        @Override
        public TestContact load() throws Exception {
            return with(Contacts._ID, mRawContact.getContactId()).loadUsingValues();
        }
    }

    public class TestGroup extends Builder<TestGroup> {

        @Override
        protected Uri getContentUri() {
            return Groups.CONTENT_URI;
        }
    }

    public class TestData extends Builder<TestData> {
        private final TestRawContact mRawContact;

        public TestData(TestRawContact rawContact, String mimeType) {
            this.mRawContact = rawContact;
            mValues.put(Data.MIMETYPE, mimeType);
        }

        @Override
        protected Uri getContentUri() {
            return Data.CONTENT_URI;
        }

        @Override
        public TestData insert() throws Exception {
            mValues.put(Data.RAW_CONTACT_ID, mRawContact.getId());
            return super.insertDependent();
        }

        public long getRawContactId() {
            return getLong(Data.RAW_CONTACT_ID);
        }

        public TestRawContact getRawContact() throws Exception {
            return mRawContact;
        }
    }

    public ContactsContract_TestDataBuilder(IContentProvider provider) {
        this.mProvider = provider;
    }

    public TestRawContact newRawContact() {
        return new TestRawContact();
    }

    public TestContact newContact() {
        return new TestContact();
    }

    public TestGroup newGroup() {
        return new TestGroup();
    }

    public void cleanup() throws Exception {
        for (Builder<?> builder : new ArrayList<Builder<?>>(mCreatedRows)) {
            builder.deletePermanently();
        }

        for (Builder<?> builder : new HashSet<Builder<?>>(mLoadedRows)) {
            builder.close();
        }
    }

}
