/*
 * 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.content.cts;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncAdapterType;
import android.os.Bundle;
import android.test.AndroidTestCase;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class ContentResolverSyncTestCase extends AndroidTestCase {

    private static final String AUTHORITY = "android.content.cts.authority";

    private static final Account ACCOUNT = new Account(MockAccountAuthenticator.ACCOUNT_NAME,
            MockAccountAuthenticator.ACCOUNT_TYPE);

    private static final int LATCH_TIMEOUT_MS = 5000;

    private static AccountManager sAccountManager;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        getMockSyncAdapter();
        sAccountManager = AccountManager.get(getContext());
    }

    @Override
    public void tearDown() throws Exception {
        getMockSyncAdapter().clearData();

        // Need to clean up created account
        removeAccount(sAccountManager, ACCOUNT, null /* callback */);

        super.tearDown();
    }

    public static synchronized MockSyncAdapter getMockSyncAdapter() {
        return MockSyncAdapter.getMockSyncAdapter();

    }

    public static synchronized MockAccountAuthenticator getMockAuthenticator(Context context) {
        return MockAccountAuthenticator.getMockAuthenticator(context);
    }

    private void addAccountExplicitly(Account account, String password, Bundle userdata) {
        assertTrue(sAccountManager.addAccountExplicitly(account, password, userdata));
    }

    private boolean removeAccount(AccountManager am, Account account,
            AccountManagerCallback<Boolean> callback) throws IOException, AuthenticatorException,
                OperationCanceledException {

        AccountManagerFuture<Boolean> futureBoolean = am.removeAccount(account,
                callback,
                null /* handler */);
        Boolean resultBoolean = futureBoolean.getResult();
        assertTrue(futureBoolean.isDone());

        return resultBoolean;
    }

    private CountDownLatch setNewLatch(CountDownLatch latch) {
        getMockSyncAdapter().setLatch(latch);
        getMockSyncAdapter().clearData();
        return latch;
    }

    private void addAccountAndVerifyInitSync(Account account, String password,
            String authority, int latchTimeoutMs) {

        CountDownLatch latch = setNewLatch(new CountDownLatch(1));

        addAccountExplicitly(account, password, null /* userData */);

        // Wait with timeout for the callback to do its work
        try {
            latch.await(latchTimeoutMs, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }

        assertFalse(getMockSyncAdapter().isStartSync());
        assertFalse(getMockSyncAdapter().isCancelSync());
        assertTrue(getMockSyncAdapter().isInitialized());
        assertEquals(account, getMockSyncAdapter().getAccount());
        assertEquals(authority, getMockSyncAdapter().getAuthority());
    }

    private void cancelSync(Account account, String authority, int latchTimeoutMillis) {
        CountDownLatch latch = setNewLatch(new CountDownLatch(1));

        Bundle extras = new Bundle();
        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);

        ContentResolver.cancelSync(account, authority);

        // Wait with timeout for the callback to do its work
        try {
            latch.await(latchTimeoutMillis, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    private void requestSync(Account account, String authority, int latchTimeoutMillis) {
        CountDownLatch latch = setNewLatch(new CountDownLatch(1));

        Bundle extras = new Bundle();
        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);

        ContentResolver.requestSync(account, authority, extras);

        // Wait with timeout for the callback to do its work
        try {
            latch.await(latchTimeoutMillis, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    private void setIsSyncable(Account account, String authority, boolean b) {
        ContentResolver.setIsSyncable(account, authority, (b) ? 1 : 0);
    }

    /**
     * Test a sync request
     */
    public void testRequestSync() throws IOException, AuthenticatorException,
            OperationCanceledException {

        // Prevent auto sync
        ContentResolver.setMasterSyncAutomatically(false);
        assertEquals(false, ContentResolver.getMasterSyncAutomatically());

        addAccountAndVerifyInitSync(ACCOUNT, MockAccountAuthenticator.ACCOUNT_PASSWORD, AUTHORITY,
                LATCH_TIMEOUT_MS);

        getMockSyncAdapter().clearData();

        setIsSyncable(ACCOUNT, AUTHORITY, true);
        cancelSync(ACCOUNT, AUTHORITY, LATCH_TIMEOUT_MS);

        getMockSyncAdapter().clearData();

        requestSync(ACCOUNT, AUTHORITY, LATCH_TIMEOUT_MS);

        assertTrue(getMockSyncAdapter().isStartSync());
        assertFalse(getMockSyncAdapter().isCancelSync());
        assertFalse(getMockSyncAdapter().isInitialized());
        assertEquals(ACCOUNT, getMockSyncAdapter().getAccount());
        assertEquals(AUTHORITY, getMockSyncAdapter().getAuthority());
    }

    /**
     * Test a sync cancel
     */
    public void testCancelSync() throws IOException, AuthenticatorException,
            OperationCanceledException {

        // Prevent auto sync
        ContentResolver.setMasterSyncAutomatically(false);
        assertEquals(false, ContentResolver.getMasterSyncAutomatically());

        addAccountAndVerifyInitSync(ACCOUNT, MockAccountAuthenticator.ACCOUNT_PASSWORD, AUTHORITY,
                LATCH_TIMEOUT_MS);

        getMockSyncAdapter().clearData();

        setIsSyncable(ACCOUNT, AUTHORITY, true);
        requestSync(ACCOUNT, AUTHORITY, LATCH_TIMEOUT_MS);

        getMockSyncAdapter().clearData();

        cancelSync(ACCOUNT, AUTHORITY, LATCH_TIMEOUT_MS);

        assertFalse(getMockSyncAdapter().isStartSync());
        assertTrue(getMockSyncAdapter().isCancelSync());
        assertFalse(getMockSyncAdapter().isInitialized());

        assertFalse(ContentResolver.isSyncActive(ACCOUNT, AUTHORITY));
        assertFalse(ContentResolver.isSyncPending(ACCOUNT, AUTHORITY));
    }

    /**
     * Test if we can set and get the MasterSyncAutomatically switch
     */
    public void testGetAndSetMasterSyncAutomatically() {
        ContentResolver.setMasterSyncAutomatically(true);
        assertEquals(true, ContentResolver.getMasterSyncAutomatically());

        ContentResolver.setMasterSyncAutomatically(false);
        assertEquals(false, ContentResolver.getMasterSyncAutomatically());
    }

    /**
     * Test if we can set and get the SyncAutomatically switch for an account
     */
    public void testGetAndSetSyncAutomatically() {
        // Prevent auto sync
        ContentResolver.setMasterSyncAutomatically(false);
        assertEquals(false, ContentResolver.getMasterSyncAutomatically());

        ContentResolver.setSyncAutomatically(ACCOUNT, AUTHORITY, false);
        assertEquals(false, ContentResolver.getSyncAutomatically(ACCOUNT, AUTHORITY));

        ContentResolver.setSyncAutomatically(ACCOUNT, AUTHORITY, true);
        assertEquals(true, ContentResolver.getSyncAutomatically(ACCOUNT, AUTHORITY));
    }

    /**
     * Test if we can set and get the IsSyncable switch for an account
     */
    public void testGetAndSetIsSyncable() {
        // Prevent auto sync
        ContentResolver.setMasterSyncAutomatically(false);
        assertEquals(false, ContentResolver.getMasterSyncAutomatically());

        addAccountExplicitly(ACCOUNT, MockAccountAuthenticator.ACCOUNT_PASSWORD, null /* userData */);

        ContentResolver.setIsSyncable(ACCOUNT, AUTHORITY, 2);
        assertTrue(ContentResolver.getIsSyncable(ACCOUNT, AUTHORITY) > 0);

        ContentResolver.setIsSyncable(ACCOUNT, AUTHORITY, 1);
        assertTrue(ContentResolver.getIsSyncable(ACCOUNT, AUTHORITY) > 0);

        ContentResolver.setIsSyncable(ACCOUNT, AUTHORITY, 0);
        assertEquals(0, ContentResolver.getIsSyncable(ACCOUNT, AUTHORITY));

        ContentResolver.setIsSyncable(ACCOUNT, AUTHORITY, -1);
        assertTrue(ContentResolver.getIsSyncable(ACCOUNT, AUTHORITY) < 0);

        ContentResolver.setIsSyncable(ACCOUNT, AUTHORITY, -2);
        assertTrue(ContentResolver.getIsSyncable(ACCOUNT, AUTHORITY) < 0);
    }

    /**
     * Test if we can get the sync adapter types
     */
    public void testGetSyncAdapterTypes() {
        SyncAdapterType[] types = ContentResolver.getSyncAdapterTypes();
        assertNotNull(types);
        int length = types.length;
        assertTrue(length > 0);
        boolean found = false;
        for (int n=0; n < length; n++) {
            SyncAdapterType type = types[n];
            if (MockAccountAuthenticator.ACCOUNT_TYPE.equals(type.accountType) &&
                    AUTHORITY.equals(type.authority)) {
                found = true;
                break;
            }
        }
        assertTrue(found);
    }

    /**
     * Test if a badly formed sync request is throwing exceptions
     */
    public void testStartSyncFailure() {
        try {
            ContentResolver.requestSync(null, null, null);
            fail("did not throw IllegalArgumentException when extras is null.");
        } catch (IllegalArgumentException e) {
            //expected.
        }
    }

    /**
     * Test validate sync extra bundle
     */
    public void testValidateSyncExtrasBundle() {
        Bundle extras = new Bundle();
        extras.putInt("Integer", 20);
        extras.putLong("Long", 10l);
        extras.putBoolean("Boolean", true);
        extras.putFloat("Float", 5.5f);
        extras.putDouble("Double", 2.5);
        extras.putString("String", MockAccountAuthenticator.ACCOUNT_NAME);
        extras.putCharSequence("CharSequence", null);

        ContentResolver.validateSyncExtrasBundle(extras);

        extras.putChar("Char", 'a'); // type Char is invalid
        try {
            ContentResolver.validateSyncExtrasBundle(extras);
            fail("did not throw IllegalArgumentException when extras is invalide.");
        } catch (IllegalArgumentException e) {
            //expected.
        }
    }
}
