/**
 * Copyright (c) 2012 Todoroo Inc
 *
 * See the file "LICENSE" for the full license governing this code.
 */
package com.todoroo.astrid.gtasks;

import java.util.Date;
import java.util.List;
import java.util.concurrent.Semaphore;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerFuture;
import android.content.Intent;
import android.os.Bundle;

import com.google.api.client.googleapis.extensions.android2.auth.GoogleAccountManager;
import com.google.api.services.tasks.model.Tasks;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.api.GtasksApiUtilities;
import com.todoroo.astrid.gtasks.api.GtasksInvoker;
import com.todoroo.astrid.gtasks.auth.GtasksTokenValidator;
import com.todoroo.astrid.gtasks.sync.GtasksSyncV2Provider;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.sync.SyncResultCallbackAdapter;
import com.todoroo.astrid.test.DatabaseTestCase;

@SuppressWarnings("nls")
public class GtasksNewSyncTest extends DatabaseTestCase {

    private static GtasksInvoker gtasksService;
    private static boolean initialized = false;
    private boolean bypassTests = false;

    private static String DEFAULT_LIST = "@default";
    private static final String TEST_ACCOUNT = "sync_tester2@astrid.com";
    private static final long TIME_BETWEEN_SYNCS = 3000l;

    @Autowired TaskService taskService;
    @Autowired MetadataService metadataService;
    @Autowired GtasksMetadataService gtasksMetadataService;
    @Autowired GtasksPreferenceService gtasksPreferenceService;

    /*
     * Basic creation tests
     */
    public void testTaskCreatedLocally() {
        if(bypassTests) return;
        String title = "Astrid task 1";
        Task localTask = createNewLocalTask(title);

        whenInvokeSync();

        assertTaskExistsRemotely(localTask, title);
    }

    public void testTaskCreatedRemotely() throws Exception {
        if(bypassTests) return;
        String title = "Gtasks task 1";
        com.google.api.services.tasks.model.Task remoteTask = new com.google.api.services.tasks.model.Task();
        remoteTask.setTitle(title);
        remoteTask = gtasksService.createGtask(DEFAULT_LIST, remoteTask);

        whenInvokeSync();

        assertTaskExistsLocally(remoteTask, title);
    }

    /*
     * Title editing tests
     */
    public void testTitleChangedLocally() throws Exception {
        if(bypassTests) return;
        String title = "Astrid task 2";
        Task localTask = createNewLocalTask(title);

        whenInvokeSync();

        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);
        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        //Set new title on local task
        String newTitle = "Astrid task 2 edited";
        localTask.setValue(Task.TITLE, newTitle);
        taskService.save(localTask);

        whenInvokeSync();

        //Refetch remote task and assert that both local and remote titles match expected
        localTask = refetchLocalTask(localTask);
        remoteTask = refetchRemoteTask(remoteTask);
        assertEquals(newTitle, localTask.getValue(Task.TITLE));
        assertEquals(newTitle, remoteTask.getTitle());
    }

    public void testTitleChangedRemotely() throws Exception {
        if(bypassTests) return;
        String title = "Astrid task 3";
        Task localTask = createNewLocalTask(title);

        whenInvokeSync();

        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);
        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        //Set new title on remote task
        String newRemoteTitle = "Task 3 edited on gtasks";
        remoteTask.setTitle(newRemoteTitle);
        gtasksService.updateGtask(DEFAULT_LIST, remoteTask);

        whenInvokeSync();

        //Refetch local/remote tasks, assert that both titles match expected
        remoteTask = refetchRemoteTask(remoteTask);
        localTask = refetchLocalTask(localTask);
        assertEquals(newRemoteTitle, remoteTask.getTitle());
        assertEquals(newRemoteTitle, localTask.getValue(Task.TITLE));
    }

    public void testDateChangedLocally() throws Exception {
        if(bypassTests) return;
        Task localTask = createLocalTaskForDateTests(" locally");
        String title = localTask.getValue(Task.TITLE);
        long startDate = localTask.getValue(Task.DUE_DATE);

        whenInvokeSync();
        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);
        localTask = refetchLocalTask(localTask);
        assertTrue(String.format("Expected %s, was %s", new Date(startDate), new Date(localTask.getValue(Task.DUE_DATE))),
                Math.abs(startDate - localTask.getValue(Task.DUE_DATE)) < 5000);
        long dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.getDue(), 0);
        long createdDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate);
        assertEquals(startDate, createdDate);
        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        //Set new due date on local task
        long newDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, new Date(116, 1, 8).getTime());
        localTask.setValue(Task.DUE_DATE, newDueDate);
        taskService.save(localTask);

        whenInvokeSync();

        //Refetch remote task and assert that both tasks match expected due date
        localTask = refetchLocalTask(localTask);
        remoteTask = refetchRemoteTask(remoteTask);
        assertEquals(newDueDate, localTask.getValue(Task.DUE_DATE).longValue());
        dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.getDue(), 0);
        createdDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate);
        assertEquals(newDueDate, createdDate);
    }

    public void testDateChangedRemotely() throws Exception {
        if(bypassTests) return;
        Task localTask = createLocalTaskForDateTests(" remotely");
        String title = localTask.getValue(Task.TITLE);
        long startDate = localTask.getValue(Task.DUE_DATE);

        whenInvokeSync();
        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);
        localTask = refetchLocalTask(localTask);
        assertTrue(String.format("Expected %s, was %s", new Date(startDate), new Date(localTask.getValue(Task.DUE_DATE))),
                Math.abs(startDate - localTask.getValue(Task.DUE_DATE)) < 5000);
        long dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.getDue(), 0);
        long createdDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate);
        assertEquals(startDate, createdDate);
        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        //Set new due date on remote task
        long newDueDate = new Date(116, 1, 8).getTime();
        remoteTask.setDue(GtasksApiUtilities.unixTimeToGtasksDueDate(newDueDate));
        newDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, newDueDate);
        gtasksService.updateGtask(DEFAULT_LIST, remoteTask);

        whenInvokeSync();

        //Refetch remote task and assert that both tasks match expected due date
        localTask = refetchLocalTask(localTask);
        remoteTask = refetchRemoteTask(remoteTask);
        assertEquals(newDueDate, localTask.getValue(Task.DUE_DATE).longValue());
        dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.getDue(), 0);
        createdDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate);
        assertEquals(newDueDate, createdDate);

    }

    public void testDateChangedBoth_ChooseLocal() throws Exception {
        if(bypassTests) return;
        Task localTask = createLocalTaskForDateTests(" remotely");
        String title = localTask.getValue(Task.TITLE);
        long startDate = localTask.getValue(Task.DUE_DATE);

        whenInvokeSync();
        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);
        localTask = refetchLocalTask(localTask);
        assertTrue(String.format("Expected %s, was %s", new Date(startDate), new Date(localTask.getValue(Task.DUE_DATE))),
                Math.abs(startDate - localTask.getValue(Task.DUE_DATE)) < 5000);
        long dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.getDue(), 0);
        long createdDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate);
        assertEquals(startDate, createdDate);
        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        //Set new due date on remote task first
        long newLocalDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, new Date(128, 5, 11).getTime());
        long newRemoteDate = new Date(121, 5, 25).getTime();

        remoteTask.setDue(GtasksApiUtilities.unixTimeToGtasksDueDate(newRemoteDate));
        gtasksService.updateGtask(DEFAULT_LIST, remoteTask);

        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        localTask.setValue(Task.DUE_DATE, newLocalDate);
        taskService.save(localTask);

        whenInvokeSync();

        //Refetch both and assert that due dates match the one we set to local (more recent)
        localTask = refetchLocalTask(localTask);
        remoteTask = refetchRemoteTask(remoteTask);
        assertEquals(newLocalDate, localTask.getValue(Task.DUE_DATE).longValue());
        dueDate = GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.getDue(), 0);
        createdDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate);
        assertEquals(newLocalDate, createdDate);
    }

    public void DISABLED_testDateChangedBoth_ChooseRemote() throws Exception {
        if(bypassTests) return;
        Task localTask = createLocalTaskForDateTests(" remotely");
        String title = localTask.getValue(Task.TITLE);
        long startDate = localTask.getValue(Task.DUE_DATE);

        whenInvokeSync();
        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);
        localTask = refetchLocalTask(localTask);
        assertTrue(String.format("Expected %s, was %s", new Date(startDate), new Date(localTask.getValue(Task.DUE_DATE))),
                Math.abs(startDate - localTask.getValue(Task.DUE_DATE)) < 5000);
        assertEquals(startDate, GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.getDue(), 0));
        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);


        //Set new due date on local task first
        long newLocalDate = new Date(128, 5, 11).getTime();
        long newRemoteDate = new Date(121, 5, 25).getTime();

        localTask.setValue(Task.DUE_DATE, newLocalDate);
        taskService.save(localTask);

        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        remoteTask.setDue(GtasksApiUtilities.unixTimeToGtasksDueDate(newRemoteDate));
        gtasksService.updateGtask(DEFAULT_LIST, remoteTask);

        whenInvokeSync();

        //Refetch both and assert that due dates match the one we set to local (more recent)
        localTask = refetchLocalTask(localTask);
        remoteTask = refetchRemoteTask(remoteTask);
        assertEquals(newLocalDate, localTask.getValue(Task.DUE_DATE).longValue());
        assertEquals(newLocalDate, GtasksApiUtilities.gtasksDueTimeToUnixTime(remoteTask.getDue(), 0));
    }

    /*
     * Helper method for due date tests
     */
    private Task createLocalTaskForDateTests(String addToTitle) {
        Task localTask = createNewLocalTask("Due date will change" + addToTitle);
        Date date = new Date(115, 2, 14);
        date.setHours(12);
        date.setMinutes(0);
        date.setSeconds(0);
        long dueDate = date.getTime();
        localTask.setValue(Task.DUE_DATE, dueDate);
        taskService.save(localTask);

        return localTask;
    }

    public void testNoteEditedLocally() throws Exception {
        if(bypassTests) return;
        Task localTask = createLocalTaskForNoteTests(" locally");
        String title = localTask.getValue(Task.TITLE);
        String originalNote = localTask.getValue(Task.NOTES);

        whenInvokeSync();

        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);
        assertEquals(originalNote, localTask.getValue(Task.NOTES));
        assertEquals(originalNote, remoteTask.getNotes());

        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        String newNote = "New local note";
        localTask.setValue(Task.NOTES, newNote);
        taskService.save(localTask);

        whenInvokeSync();

        localTask = refetchLocalTask(localTask);
        remoteTask = refetchRemoteTask(remoteTask);
        assertEquals(newNote, localTask.getValue(Task.NOTES));
        assertEquals(newNote, remoteTask.getNotes());
    }

    public void testNoteEditedRemotely() throws Exception {
        if(bypassTests) return;
        Task localTask = createLocalTaskForNoteTests(" remotely");
        String title = localTask.getValue(Task.TITLE);
        String originalNote = localTask.getValue(Task.NOTES);

        whenInvokeSync();

        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);
        assertEquals(originalNote, localTask.getValue(Task.NOTES));
        assertEquals(originalNote, remoteTask.getNotes());

        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        String newNote = "New remote note";
        remoteTask.setNotes(newNote);
        gtasksService.updateGtask(DEFAULT_LIST, remoteTask);

        whenInvokeSync();

        localTask = refetchLocalTask(localTask);
        remoteTask = refetchRemoteTask(remoteTask);
        assertEquals(newNote, localTask.getValue(Task.NOTES));
        assertEquals(newNote, remoteTask.getNotes());
    }

    public void DISABLED_testNoteEditedBoth() throws Exception {
        if(bypassTests) return;
        Task localTask = createLocalTaskForNoteTests(" remotely");
        String title = localTask.getValue(Task.TITLE);
        String originalNote = localTask.getValue(Task.NOTES);

        whenInvokeSync();

        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);
        assertEquals(originalNote, localTask.getValue(Task.NOTES));
        assertEquals(originalNote, remoteTask.getNotes());

        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        String newLocalNote = "New local note";
        String newRemoteNote = "New remote note";

        localTask.setValue(Task.NOTES, newLocalNote);
        taskService.save(localTask);

        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        remoteTask.setNotes(newRemoteNote);
        gtasksService.updateGtask(DEFAULT_LIST, remoteTask);

        whenInvokeSync();

        localTask = refetchLocalTask(localTask);
        remoteTask = refetchRemoteTask(remoteTask);
        System.err.println("Local note: " + localTask.getValue(Task.NOTES));
        System.err.println("Remote note: " + remoteTask.getNotes());
    }

    private Task createLocalTaskForNoteTests(String addToTitle) {
        Task localTask = createNewLocalTask("Note will change" + addToTitle);
        String note = "Original note";
        localTask.setValue(Task.NOTES, note);
        taskService.save(localTask);

        return localTask;
    }

    /*
     * Completion tests
     */

    public void testTaskCompletedLocally() throws Exception {
        if(bypassTests) return;
        String title = "Will complete locally";
        Task localTask = createNewLocalTask(title);

        whenInvokeSync();
        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);

        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        long completion = DateUtilities.now();
        localTask.setValue(Task.COMPLETION_DATE, completion);
        taskService.save(localTask);

        whenInvokeSync();

        localTask = refetchLocalTask(localTask);
        remoteTask = refetchRemoteTask(remoteTask);
        assertTrue(String.format("Expected %s, was %s", new Date(completion), new Date(localTask.getValue(Task.COMPLETION_DATE))),
                Math.abs(completion - localTask.getValue(Task.COMPLETION_DATE)) < 5000);
        assertEquals("completed", remoteTask.getStatus());
    }

    public void testTaskCompletedRemotely() throws Exception {
        if(bypassTests) return;
        String title = "Will complete remotely";
        Task localTask = createNewLocalTask(title);

        whenInvokeSync();
        com.google.api.services.tasks.model.Task remoteTask = assertTaskExistsRemotely(localTask, title);

        AndroidUtilities.sleepDeep(TIME_BETWEEN_SYNCS);

        long completion = DateUtilities.now();
        remoteTask.setStatus("completed");
        remoteTask.setCompleted(GtasksApiUtilities.unixTimeToGtasksCompletionTime(completion));
        gtasksService.updateGtask(DEFAULT_LIST, remoteTask);

        whenInvokeSync();

        localTask = refetchLocalTask(localTask);
        remoteTask = refetchRemoteTask(remoteTask);
        assertTrue(String.format("Expected %s, was %s", new Date(completion), new Date(localTask.getValue(Task.COMPLETION_DATE))),
                Math.abs(completion - localTask.getValue(Task.COMPLETION_DATE)) < 5000);
        assertEquals("completed", remoteTask.getStatus());
    }

    private com.google.api.services.tasks.model.Task assertTaskExistsRemotely(Task localTask, String title) {
        //Get the corresponding remote id for a local task
        Metadata metadata = gtasksMetadataService.getTaskMetadata(localTask.getId());
        String taskId = metadata.getValue(GtasksMetadata.ID);
        String listId = metadata.getValue(GtasksMetadata.LIST_ID);

        //Fetch the remote task belonging to that id
        com.google.api.services.tasks.model.Task remoteTask = null;
        try {
            remoteTask = gtasksService.getGtask(listId, taskId);
        } catch (Exception e) {
            e.printStackTrace();
            fail("Failed to find remote task " + taskId);
        }

        //Do a basic title match
        assertNotNull(remoteTask);
        assertEquals(title, localTask.getValue(Task.TITLE));
        assertEquals(title, remoteTask.getTitle());
        return remoteTask;
    }

    private Task assertTaskExistsLocally(com.google.api.services.tasks.model.Task remoteTask, String title) {
        long localId = localIdForTask(remoteTask);

        //Fetch the local task from the database
        Task localTask = taskService.fetchById(localId, Task.PROPERTIES);

        assertNotNull(localTask);
        assertEquals(title, remoteTask.getTitle());
        assertEquals(title, localTask.getValue(Task.TITLE));
        return localTask;
    }

    private Task refetchLocalTask(Task localTask) {
        return taskService.fetchById(localTask.getValue(Task.ID), Task.PROPERTIES);
    }

    private com.google.api.services.tasks.model.Task refetchRemoteTask(com.google.api.services.tasks.model.Task remoteTask) throws Exception {
        return gtasksService.getGtask(DEFAULT_LIST, remoteTask.getId());
    }

    private long localIdForTask(com.google.api.services.tasks.model.Task remoteTask) {
        TodorooCursor<Metadata> cursor = metadataService.query(Query.select(Metadata.TASK).
                where(Criterion.and(Metadata.KEY.eq(GtasksMetadata.METADATA_KEY), GtasksMetadata.ID.eq(remoteTask.getId()))));
        try {
            assertEquals(1, cursor.getCount());

            cursor.moveToFirst();
            return cursor.get(Metadata.TASK);
        } finally {
            cursor.close();
        }
    }


  //Create a new Astrid task and save it to the database
    private Task createNewLocalTask(String title) {
        Task task = new Task();
        task.setValue(Task.TITLE, title);
        taskService.save(task);
        return task;
    }

    //Perform a synchronization
    private void whenInvokeSync() {
    	final Semaphore sema = new Semaphore(0);
        GtasksSyncV2Provider.getInstance().synchronizeActiveTasks(true, new SyncResultCallbackAdapter() {
        	@Override
        	public void finished() {
        		sema.release();
        	}
		});
        try {
        	sema.acquire();
        } catch (InterruptedException e) {
        	fail("Interrupted while waiting for sync to finish");
        }
    }

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

        if (!initialized) {
            initializeTestService();
        }

        setupTestList();
    }

    private void initializeTestService() throws Exception {
        GoogleAccountManager manager = new GoogleAccountManager(ContextManager.getContext());
        Account[] accounts = manager.getAccounts();

        Account toUse = null;
        for (Account a : accounts) {
            if (a.name.equals(TEST_ACCOUNT)) {
                toUse = a;
                break;
            }
        }
        if (toUse == null) {
            if (accounts.length == 0) {
                bypassTests = true;
                return;
            }
            toUse = accounts[0];
        }

        Preferences.setString(GtasksPreferenceService.PREF_USER_NAME, toUse.name);
        AccountManagerFuture<Bundle> accountManagerFuture = manager.manager.getAuthToken(toUse, "oauth2:https://www.googleapis.com/auth/tasks", true, null, null);

        Bundle authTokenBundle = accountManagerFuture.getResult();
        if (authTokenBundle.containsKey(AccountManager.KEY_INTENT)) {
            Intent i = (Intent) authTokenBundle.get(AccountManager.KEY_INTENT);
            ContextManager.getContext().startActivity(i);
            return;
        }
        String authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN);
        authToken = GtasksTokenValidator.validateAuthToken(getContext(), authToken);
        gtasksPreferenceService.setToken(authToken);

        gtasksService = new GtasksInvoker(authToken);

        initialized = true;
    }

    private void setupTestList() throws Exception {
        Tasks defaultListTasks = gtasksService.getAllGtasksFromListId(DEFAULT_LIST, false, false, 0);
        List<com.google.api.services.tasks.model.Task> items = defaultListTasks.getItems();
        if (items != null) {
            for (com.google.api.services.tasks.model.Task t : items) {
                gtasksService.deleteGtask(DEFAULT_LIST, t.getId());
            }
        }
    }

}
