/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.netbeans.modules.bugzilla;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertSame;
import static junit.framework.Assert.assertTrue;
import junit.framework.Test;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.mylyn.internal.bugzilla.core.BugzillaOperation;
import org.eclipse.mylyn.internal.bugzilla.core.BugzillaRepositoryConnector;
import org.eclipse.mylyn.internal.tasks.core.data.TaskDataManager;
import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.ITaskMapping;
import org.eclipse.mylyn.tasks.core.TaskMapping;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.core.data.TaskOperation;
import org.netbeans.junit.NbModuleSuite;
import org.netbeans.junit.NbTestCase;
import org.netbeans.modules.bugtracking.util.BugtrackingUtil;
import static org.netbeans.modules.bugzilla.TestConstants.REPO_PASSWD;
import static org.netbeans.modules.bugzilla.TestConstants.REPO_URL;
import static org.netbeans.modules.bugzilla.TestConstants.REPO_USER;
import org.netbeans.modules.bugzilla.repository.BugzillaRepository;
import org.netbeans.modules.mylyn.util.commands.GetRepositoryTasksCommand;
import org.netbeans.modules.mylyn.util.MylynSupport;
import org.netbeans.modules.mylyn.util.NbTask;
import org.netbeans.modules.mylyn.util.NbTask.SynchronizationState;
import org.netbeans.modules.mylyn.util.NbTaskDataModel;
import org.netbeans.modules.mylyn.util.NbTaskDataState;
import org.netbeans.modules.mylyn.util.commands.SimpleQueryCommand;
import org.netbeans.modules.mylyn.util.SubmitCommand;
import org.netbeans.modules.mylyn.util.commands.SubmitTaskCommand;
import org.netbeans.modules.mylyn.util.commands.SynchronizeQueryCommand;
import org.netbeans.modules.mylyn.util.commands.SynchronizeTasksCommand;
import org.netbeans.modules.mylyn.util.TaskDataListener;
import org.netbeans.modules.mylyn.util.NbTaskListener;
import org.openide.util.RequestProcessor;
import org.openide.util.test.MockLookup;

/**
 *
 * @author Ondrej Vrabec
 */
public class MylynStorageTest extends NbTestCase {
    
    private static final String QUERY_NAME = "My new query";
    private BugzillaRepository br;
    private TaskRepository btr;
    
    private static final String PRODUCT = "mylyn"; //NOI18N
    private static final String COMPONENT = "default"; //NOI18N
    
    public MylynStorageTest (String arg0) {
        super(arg0);
    }

    @Override
    protected Level logLevel() {
        return Level.ALL;
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        System.setProperty("netbeans.user", new File(getDataDir(), "userdir").getAbsolutePath());
        MockLookup.setLayersAndInstances();
        BugtrackingUtil.getBugtrackingConnectors(); // ensure conector
        
        // reset
        Method m = MylynSupport.class.getDeclaredMethod("reset", new Class[0]);
        m.setAccessible(true);
        m.invoke(MylynSupport.class);
                
        Field f = Bugzilla.class.getDeclaredField("instance");
        f.setAccessible(true);
        f.set(Bugzilla.class, null);
        
        br = TestUtil.getRepository("testbugzilla", REPO_URL, REPO_USER, REPO_PASSWD);
        btr = br.getTaskRepository();
    }

    @Override
    protected void tearDown () throws Exception {
        // persist for next round and shutdown
        Method m = MylynSupport.class.getDeclaredMethod("finish");
        m.setAccessible(true);
        m.invoke(MylynSupport.getInstance());
        
        super.tearDown();
    }
    
    public static Test suite () {
        return NbModuleSuite.emptyConfiguration()  
                
        .addTest(MylynStorageTest.class,
                // creates an offline temporary task
                "testCreateUnsubmittedTask",
                // submit the temporary task to the server and turn it into a full remote task
                "testSubmitTemporaryTask",
                // repository settings modifications should keep the single instance of TR
                "testEditRepository",
                // edit task
                "testEditTask",
                // submit task
                "testSubmitTask",
                // external changes
                "testIncomingChanges",
                // external changes and refresh in editor page
                "testIncomingChangesInEditorPage",
                // conflicts in incoming and outgoing
                "testConflicts",
                // conflicts in incoming and outgoing in editor page
                "testConflictsInEditorPage",
                // open task editor for unknown task
                "testOpenUnknownTask",
                // open task editor for task with deleted/corrupted task data in storage
                "testOpenTaskWithDeletedData",

                // create and init query
                "testCreateQuery",
                // synchronize and get external changes
                "testSynchronizeQuery",
                // modify query
                "testModifyQuery",
                // remove from query internal - closing a task
                "testTaskRemovedFromQueryInt",
                // remove from query externally - closing a task
                "testTaskRemovedFromQueryExt",
                // test simple search - temporary query not added to the tasklist
                "testSimpleSearch"
                ).gui(false)
                .suite();
    }
    
    public void testCreateUnsubmittedTask () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        ITaskMapping mapping = new TaskMapping() {

            @Override
            public String getProduct () {
                return PRODUCT;
            }

            @Override
            public String getComponent () {
                return COMPONENT;
            }
            
        };
        NbTask task = supp.createTask(btr, mapping);
        Collection<NbTask> allLocalTasks = supp.getTasks(supp.getLocalTaskRepository());
        Collection<NbTask> allUnsubmittedTasks = supp.getUnsubmittedTasksContainer(btr).getTasks();

        /*************** TEST *******************/
        // is it really in the tasklist
        assertEquals(0, allLocalTasks.size());
//        assertTrue(allLocalTasks.contains(task));
        assertEquals(1, allUnsubmittedTasks.size());
        assertTrue(allUnsubmittedTasks.contains(task));
        assertEquals(0, supp.getTasks(btr).size()); // not yet in the repository
        assertEquals("1", task.getTaskId());
    }
    
    public void testSubmitTemporaryTask () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        NbTask task = supp.getUnsubmittedTasksContainer(btr).getTasks().iterator().next();
        // edit the task
        NbTaskDataModel model = task.getTaskDataModel();
        
        // model.getTaskData returns our local data
        String defaultSummary = task.getSummary();
        TaskAttribute rta = model.getLocalTaskData().getRoot();
        assertFalse(model.isDirty());
        // now edit summary, product and component
        String newSummary = "Task summary testSubmitTemporaryTask";
        TaskAttribute ta = rta.getMappedAttribute(TaskAttribute.SUMMARY);
        ta.setValue(newSummary);
        model.attributeChanged(ta);
        
        // now we have unsaved changes, the task is dirty
        assertTrue(model.isDirty());
        // not yet saved
        assertEquals(defaultSummary, task.getSummary());
        // save
        model.save(new NullProgressMonitor());
        // all saved?
        assertFalse(model.isDirty());
        // well, not exactly, for new unsubmitted task we need to manually refresh task's attributes
        assertEquals(defaultSummary, task.getSummary());
        if (task.getSynchronizationState() == SynchronizationState.OUTGOING_NEW) {
            task.setSummary(newSummary);
        }
        assertEquals(newSummary, task.getSummary());
        
        // let's submit finally
        NbTask submittedTask = submitTask(task, model);
        
        assertNotSame(task, submittedTask); // they difer, the new task is a persistent, not local one
        assertEquals(0, supp.getUnsubmittedTasksContainer(btr).getTasks().size());
        assertSame(submittedTask, supp.getTask(btr.getUrl(), submittedTask.getTaskId()));
        
        assertEquals(newSummary, task.getSummary());
        model = submittedTask.getTaskDataModel();
        assertSame(btr, model.getTaskRepository());
        assertSame(submittedTask, model.getTask());
        assertFalse(model.isDirty());
        assertTrue(model.hasBeenRead());
        assertTrue(model.getChangedAttributes().isEmpty());
        assertTrue(model.getChangedOldAttributes().isEmpty());
        assertEquals(SynchronizationState.SYNCHRONIZED, submittedTask.getSynchronizationState());
    }
    
    public void testEditRepository () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        Collection<NbTask> tasks = supp.getTasks(btr);
        assertEquals(1, tasks.size());
        NbTask task = tasks.iterator().next();
        assertNotNull(task);
        
        // TR should be singleton
        BugzillaRepository otherRepository = TestUtil.getRepository("testbugzilla", REPO_URL, REPO_USER, REPO_PASSWD);
        TaskRepository otherTaskRepository = otherRepository.getTaskRepository();
        assertSame(btr, otherTaskRepository);
        assertNotNull(task.getTaskDataState());
        
        // now lets change URL, it should propagate to the tasklist and rewrite all tasks
        Method m = BugzillaRepository.class.getDeclaredMethod("setupTaskRepository",
                String.class, String.class, String.class, String.class,
                char[].class, String.class, char[].class,
                Boolean.TYPE);
        m.setAccessible(true);
        m.invoke(otherRepository, "testbugzilla", REPO_URL, REPO_URL + "/OTHER",
                REPO_USER, REPO_PASSWD.toCharArray(), null, null, false);
        assertSame(btr, otherTaskRepository);
        assertEquals(REPO_URL + "/OTHER", otherRepository.getUrl());
        assertEquals(otherRepository.getUrl(), task.getRepositoryUrl());
        assertNotNull(task.getTaskDataState());
        NbTask task2 = supp.getTasks(otherTaskRepository).iterator().next();
        assertSame(task, task2);
        assertEquals(otherRepository.getUrl(), task2.getRepositoryUrl());
        assertNotNull(task2.getTaskDataState());
        
        // and back to clean
        m.invoke(otherRepository, "testbugzilla", REPO_URL + "/OTHER", REPO_URL,
                REPO_USER, REPO_PASSWD.toCharArray(), null, null, false);
        assertEquals(REPO_URL, otherRepository.getUrl());
        assertEquals(otherRepository.getUrl(), task.getRepositoryUrl());
    }
    
    public void testEditTask () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        Collection<NbTask> tasks = supp.getTasks(btr);
        assertEquals(1, tasks.size());
        NbTask task = tasks.iterator().next();
        assertNotNull(task);
        
        // the task should be clean, synchronized and without any modifications
        NbTaskDataModel model = task.getTaskDataModel();
        assertFalse(model.isDirty());
        assertEquals(SynchronizationState.SYNCHRONIZED, task.getSynchronizationState());
        // edit
        TaskAttribute rta = model.getLocalTaskData().getRoot();
        String oldSummary = task.getSummary();
        String newSummary = getName() + "_" + task.getTaskId();
        // change the task summary
        TaskAttribute summaryAttr = rta.getMappedAttribute(TaskAttribute.SUMMARY);
        summaryAttr.setValue(newSummary);
        model.attributeChanged(summaryAttr);
        // task is dirty - not saved and has modifications
        assertTrue(model.isDirty());
        assertEquals(1, model.getChangedAttributes().size());
        for (TaskAttribute attr : model.getChangedAttributes()) {
            // unsaved modifications
            assertEquals(newSummary, attr.getValue());
            // no outgoing until save
            assertFalse(model.hasOutgoingChanges(attr));
        }
        
        // unsubmitted tasks should be empty
        assertEquals(0, br.getUnsubmittedIssues().size());
        final CountDownLatch l = new CountDownLatch(1);
        PropertyChangeListener list = new PropertyChangeListener() {
            @Override
            public void propertyChange (PropertyChangeEvent evt) {
                l.countDown();
            }
        };
        br.addPropertyChangeListener(list);
        
        // save
        model.save(new NullProgressMonitor());
        l.await();
        assertEquals(1, br.getUnsubmittedIssues().size());
        br.removePropertyChangeListener(list);
        
        // task is clean (saved) - and has modifications
        assertFalse(model.isDirty());
        assertEquals(1, model.getChangedAttributes().size());
        for (TaskAttribute attr : model.getChangedAttributes()) {
            // unsaved modifications
            assertEquals(newSummary, attr.getValue());
            // no outgoing until save
            assertTrue(model.hasOutgoingChanges(attr));
        }
        assertEquals(SynchronizationState.OUTGOING, task.getSynchronizationState());
    }
    
    public void testSubmitTask () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        Collection<NbTask> tasks = supp.getTasks(btr);
        assertEquals(1, tasks.size());
        NbTask task = tasks.iterator().next();
        assertNotNull(task);
        
        // outgoing unsubmitted changes
        assertEquals(SynchronizationState.OUTGOING, task.getSynchronizationState());
        NbTaskDataModel model = task.getTaskDataModel();
        String oldSummary = task.getSummary();
        TaskAttribute summaryAttr = model.getLocalTaskData().getRoot().getMappedAttribute(TaskAttribute.SUMMARY);
        String newSummary = summaryAttr.getValue();
        assertTrue(model.hasOutgoingChanges(summaryAttr));
        assertFalse(oldSummary.equals(newSummary));
        
        // unsubmitted tasks should contain the task
        assertEquals(1, br.getUnsubmittedIssues().size());
        final CountDownLatch l = new CountDownLatch(1);
        PropertyChangeListener list = new PropertyChangeListener() {
            @Override
            public void propertyChange (PropertyChangeEvent evt) {
                l.countDown();
            }
        };
        br.addPropertyChangeListener(list);
        
        // submit
        task = submitTask(task, model);
        l.await();
        assertEquals(0, br.getUnsubmittedIssues().size());
        br.removePropertyChangeListener(list);
        
        // test
        assertFalse(model.isDirty());
        assertTrue(model.getChangedAttributes().isEmpty());
        assertTrue(model.getChangedOldAttributes().isEmpty());
        assertEquals(SynchronizationState.SYNCHRONIZED, task.getSynchronizationState());
        assertEquals(newSummary, task.getSummary());
    }
    
    public void testIncomingChanges () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        Collection<NbTask> tasks = supp.getTasks(btr);
        assertEquals(1, tasks.size());
        NbTask task = tasks.iterator().next();
        assertNotNull(task);
        
        DummyTaskWrapper wrapper = new DummyTaskWrapper(task);
        String oldSummary = task.getSummary();
        String newSummary = getName() + "_" + task.getTaskId();
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        makeExternalChange(task, newSummary);
        
        // still no change, need to do a sync job
        assertEquals(oldSummary, wrapper.getSummary());
        assertEquals(SynchronizationState.SYNCHRONIZED, task.getSynchronizationState());
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        
        
        // sync with server
        SynchronizeTasksCommand cmd = supp.getCommandFactory().createSynchronizeTasksCommand(btr, Collections.<NbTask>singleton(task));
        br.getExecutor().execute(cmd);
        assertEquals(SynchronizationState.INCOMING, task.getSynchronizationState());
        assertEquals(SynchronizationState.INCOMING, wrapper.getSynchronizationState());
        assertEquals(newSummary, wrapper.getSummary());
        assertEquals("Summary from " + oldSummary + " to " + newSummary, wrapper.getIncomingChangesText());
        wrapper.forget();
    }
    
    public void testIncomingChangesInEditorPage () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        Collection<NbTask> tasks = supp.getTasks(btr);
        assertEquals(1, tasks.size());
        NbTask task = tasks.iterator().next();
        assertNotNull(task);
        
        String oldSummary = task.getSummary();
        String newSummary = getName() + "_" + task.getTaskId();
        DummyTaskWrapper wrapper = new DummyTaskWrapper(task);
        assertEquals(SynchronizationState.INCOMING, wrapper.getSynchronizationState());
        DummyEditorPage page = new DummyEditorPage(task);
        page.open();
        page.assertOpened();
        assertEquals(oldSummary, page.taskDataSummary);
        assertTrue(page.summaryChanged);
        page.clear();
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        makeExternalChange(task, newSummary);
        
        // still no change, need to do a sync job
        assertEquals(oldSummary, wrapper.getSummary());
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        
        
        // sync with server
        SynchronizeTasksCommand cmd = supp.getCommandFactory().createSynchronizeTasksCommand(btr, Collections.<NbTask>singleton(task));
        br.getExecutor().execute(cmd);
        // synchronized because it's refreshed in the editor page automatically
        assertTrue(page.summaryChanged);
        assertEquals(newSummary, page.taskDataSummary);
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        assertEquals(newSummary, wrapper.getSummary());
        assertEquals("", wrapper.getIncomingChangesText());
        
        page.close();
        wrapper.forget();
    }
    
    public void testConflicts () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        Collection<NbTask> tasks = supp.getTasks(btr);
        assertEquals(1, tasks.size());
        NbTask task = tasks.iterator().next();
        assertNotNull(task);
        
        DummyTaskWrapper wrapper = new DummyTaskWrapper(task);
        String oldSummary = task.getSummary();
        String newSummary = getName() + "_" + task.getTaskId();
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        makeExternalChange(task, newSummary);
        
        // still no change, need to do a sync job
        assertEquals(oldSummary, wrapper.getSummary());
        assertEquals(SynchronizationState.SYNCHRONIZED, task.getSynchronizationState());
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        
        // make local changes
        DummyEditorPage page = new DummyEditorPage(task);
        page.open();
        page.assertOpened();
        page.changeSummary(newSummary + "_local");
        // save
        page.close();
        
        // still no change, need to do a sync job
        assertEquals(SynchronizationState.OUTGOING, wrapper.getSynchronizationState());
        
        // sync with server
        SynchronizeTasksCommand cmd = supp.getCommandFactory().createSynchronizeTasksCommand(btr, Collections.<NbTask>singleton(task));
        br.getExecutor().execute(cmd);
        assertEquals(SynchronizationState.CONFLICT, wrapper.getSynchronizationState());
        assertEquals(newSummary, wrapper.getSummary());
        assertEquals("Summary from " + oldSummary + " to " + newSummary, wrapper.getIncomingChangesText());
        
        // open editor and clear 
        page.open();
        assertEquals(SynchronizationState.OUTGOING, wrapper.getSynchronizationState());
        assertEquals("", wrapper.getIncomingChangesText());
        assertTrue(page.summaryChangedLocally);
        
        // revert to synchronized
        page.revert();
        assertEquals(SynchronizationState.SYNCHRONIZED, task.getSynchronizationState());
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        
        wrapper.forget();
    }
    
    public void testConflictsInEditorPage () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        Collection<NbTask> tasks = supp.getTasks(btr);
        assertEquals(1, tasks.size());
        NbTask task = tasks.iterator().next();
        assertNotNull(task);
        
        DummyTaskWrapper wrapper = new DummyTaskWrapper(task);
        String oldSummary = task.getSummary();
        String newSummary = getName() + "_" + task.getTaskId();
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        makeExternalChange(task, newSummary);
        
        // still no change, need to do a sync job
        assertEquals(oldSummary, wrapper.getSummary());
        assertEquals(SynchronizationState.SYNCHRONIZED, task.getSynchronizationState());
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        
        // make local changes
        DummyEditorPage page = new DummyEditorPage(task);
        page.open();
        page.assertOpened();
        page.changeSummary(newSummary + "_local");
        // save
        page.save();
        
        // still no change, need to do a sync job
        assertEquals(SynchronizationState.OUTGOING, wrapper.getSynchronizationState());
        
        // sync with server
        SynchronizeTasksCommand cmd = supp.getCommandFactory().createSynchronizeTasksCommand(btr, Collections.<NbTask>singleton(task));
        br.getExecutor().execute(cmd);
        // not in conflict because it's refreshed in the editor page automatically
        assertTrue(page.summaryChangedLocally);
        assertEquals(newSummary + "_local", page.taskDataSummary);
        assertEquals(SynchronizationState.OUTGOING, wrapper.getSynchronizationState());
        assertEquals(newSummary, wrapper.getSummary());
        assertEquals("", wrapper.getIncomingChangesText());
        
        // revert to synchronized
        page.revert();
        assertEquals(SynchronizationState.SYNCHRONIZED, task.getSynchronizationState());
        assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
        assertEquals(newSummary, page.taskDataSummary);
        assertEquals(newSummary, wrapper.getSummary());
        assertEquals("", wrapper.getIncomingChangesText());
        
        page.close();
        wrapper.forget();
    }
    
    public void testOpenUnknownTask () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        
        String taskId = "1";
        Collection<NbTask> tasks = supp.getTasks(btr);
        for (NbTask t : tasks) {
            assertFalse(taskId.equals(t.getTaskId()));
        }
        
        DummyEditorPage page = new DummyEditorPage(taskId);
        page.open();
        page.waitUntilOpened();
        page.assertOpened();
        
        page.close();
        
        assertTrue(supp.getTasks(btr).contains(page.task));
        page.task.delete();
        assertFalse(supp.getTasks(btr).contains(page.task));
    }
    
    public void testOpenTaskWithDeletedData () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        
        Collection<NbTask> tasks = supp.getTasks(btr);
        assertEquals(1, tasks.size());
        NbTask task = tasks.iterator().next();
        assertNotNull(task);
        // delete task data
        assertNotNull(task.getTaskDataState());
        deleteTaskData(task);
        assertNull(task.getTaskDataState());
        
        DummyEditorPage page = new DummyEditorPage(task);
        page.open();
        page.waitUntilOpened();
        page.assertOpened();
        assertEquals(task, page.task);
        
        page.close();
    }
    
    public void testCreateQuery () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        
        Collection<NbTask> tasks = supp.getTasks(btr);
        assertEquals(1, tasks.size());
        // task already known, will be up to date
        NbTask task = tasks.iterator().next();
        assertNotNull(task);
                
        // query list is empty
        assertEquals(0, supp.getRepositoryQueries(btr).size());
        // create new query
        final IRepositoryQuery query = supp.createNewQuery(btr, QUERY_NAME);
        query.setUrl("/buglist.cgi?query_format=advanced&product=" + PRODUCT + "&component=" + COMPONENT);
        supp.addQuery(btr, query);
        // was it added?
        assertSame(query, supp.getRepositoryQueries(btr).iterator().next());
        
        // it's still empty, need to sync first
        assertEquals(0, supp.getTasks(query).size());
        
        DummyQueryController controller = new DummyQueryController(query);
        // synchronize
        SynchronizeQueryCommand cmd = supp.getCommandFactory().createSynchronizeQueriesCommand(btr, query);
        cmd.addCommandProgressListener(controller);
        br.getExecutor().execute(cmd);
        
        // all pages should be opened
        controller.closeAllPages();
        // get all tasks for the query
        tasks = controller.getTasks();
        assertEquals(new HashSet<NbTask>(supp.getTasks(query)), new HashSet<NbTask>(tasks));
        assertTrue(tasks.contains(task));
        
        // all tasks are NEW - except for the known old task
        for (NbTask t : tasks) {
            if (t == task || controller.getOpenedTasks().contains(t)) {
                assertEquals(SynchronizationState.SYNCHRONIZED, t.getSynchronizationState());
            } else {
                if (t.getSynchronizationState() == SynchronizationState.SYNCHRONIZED) {
                    // prehistoric tasks are marked as synchronized
                    // but have a specific flag
                    assertEquals("true", t.getAttribute("NetBeans.task.unseen"));
                } else {
                    assertEquals(SynchronizationState.INCOMING_NEW, t.getSynchronizationState());
                }
                DummyEditorPage p = new DummyEditorPage(t);
                p.open();
                p.assertOpened();
                p.close();
                assertEquals(SynchronizationState.SYNCHRONIZED, t.getSynchronizationState());
                assertNull(t.getAttribute("NetBeans.task.unseen"));
            }
        }
    }
    
    public void testSynchronizeQuery () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        IRepositoryQuery q = supp.getRepositoryQuery(btr, QUERY_NAME);
        Collection<NbTask> tasks = supp.getTasks(q);
        Set<DummyTaskWrapper> wrappers = new HashSet<DummyTaskWrapper>(tasks.size());
        
        // get tasks from the query
        assertFalse(tasks.isEmpty());
        
        // make external changes in summaries
        Map<NbTask, String> oldSummaries = new HashMap<NbTask, String>(tasks.size());
        Map<NbTask, String> newSummaries = new HashMap<NbTask, String>(tasks.size());
        int i = 0;
        for (NbTask task : tasks) {
            // make at most 10 changes so this ends sometimes
            if (++i > 10) {
                break;
            }
            wrappers.add(new DummyTaskWrapper(task));
            assertEquals(SynchronizationState.SYNCHRONIZED, task.getSynchronizationState());
            String newSummary = getName() + "_" + task.getTaskId() + "_" + System.currentTimeMillis();
            oldSummaries.put(task, task.getSummary());
            newSummaries.put(task, newSummary);
            makeExternalChange(task, newSummary);
        }
        
        // no change yet
        for (DummyTaskWrapper wrapper : wrappers) {
            assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
            assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.task.getSynchronizationState());
        }
        SynchronizeQueryCommand cmd = supp.getCommandFactory().createSynchronizeQueriesCommand(btr, q);
        br.getExecutor().execute(cmd);
          
        // all tasks have incoming changes
        for (DummyTaskWrapper wrapper : wrappers) {
            String newSummary = newSummaries.get(wrapper.task);
            assertEquals(SynchronizationState.INCOMING, wrapper.getSynchronizationState());
            assertEquals(SynchronizationState.INCOMING, wrapper.task.getSynchronizationState());
            assertEquals(newSummary, wrapper.getSummary());
            assertEquals("Summary from " + oldSummaries.get(wrapper.task) + " to " + newSummary, wrapper.getIncomingChangesText());
            
            // open and see changes
            DummyEditorPage p = new DummyEditorPage(wrapper.task);
            p.open();
            assertTrue(p.summaryChanged);
            assertEquals(newSummary, p.taskDataSummary);
            assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.getSynchronizationState());
            assertEquals(SynchronizationState.SYNCHRONIZED, wrapper.task.getSynchronizationState());
            p.close();
        }
        for (DummyTaskWrapper wrapper : wrappers) {
            wrapper.forget();
        }
    }
    
    public void testModifyQuery () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        IRepositoryQuery query = supp.getRepositoryQuery(btr, QUERY_NAME);
        Collection<NbTask> tasks = supp.getTasks(query);
        Collection<NbTask> toRemove = new HashSet<NbTask>();
        for (NbTask task : tasks) {
            if (task.isCompleted()) {
                toRemove.add(task);
            }
        }
        assertFalse(toRemove.isEmpty());
        
        // make another new task so the query is not empty
        NbTask createdTask = createNewTask("New task testModifyQuery");
        
        // modify query to make it more precise - will not list closed tasks
        assertFalse(tasks.isEmpty());
        DummyQueryController controller = new DummyQueryController(query);
        query.setUrl(query.getUrl() + "&bug_status=NEW" + "&bug_status=REOPENED"); //NOI18N
        // synchronize
        SynchronizeQueryCommand cmd = supp.getCommandFactory().createSynchronizeQueriesCommand(btr, query);
        cmd.addCommandProgressListener(controller);
        br.getExecutor().execute(cmd);
        
        tasks = controller.tasks;
        assertFalse(tasks.isEmpty());
        assertEquals(new HashSet<NbTask>(supp.getTasks(query)), new HashSet<NbTask>(tasks));
        for (NbTask removedTask : toRemove) {
            assertFalse(tasks.contains(removedTask));
        }
        assertTrue(tasks.contains(createdTask));
    }
    
    public void testTaskRemovedFromQueryInt () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        IRepositoryQuery q = supp.getRepositoryQuery(btr, QUERY_NAME);
        Collection<NbTask> tasks = supp.getTasks(q);
        
        // get tasks from the query
        assertFalse(tasks.isEmpty());
        DummyQueryController controller = new DummyQueryController(q);
        // get a task to close
        NbTask task = tasks.iterator().next();
        assertTrue(controller.tasks.contains(task));
        // close the task in editor
        DummyEditorPage page = new DummyEditorPage(task);
        page.open();
        page.closeTask("WONTFIX");
        page.save();
        
        // submit
        page.submit();
        assertTrue(task.isCompleted());
        
        // refresh query
        SynchronizeQueryCommand cmd = supp.getCommandFactory().createSynchronizeQueriesCommand(btr, q);
        cmd.addCommandProgressListener(controller);
        br.getExecutor().execute(cmd);
        
        // task should be removed from the list
        assertFalse(controller.getTasks().contains(task));
        
        page.close();
    }
    
    public void testTaskRemovedFromQueryExt () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        IRepositoryQuery q = supp.getRepositoryQuery(btr, QUERY_NAME);
        Collection<NbTask> tasks = supp.getTasks(q);
        
        // get tasks from the query
        assertFalse(tasks.isEmpty());
        DummyQueryController controller = new DummyQueryController(q);
        // get a task to close
        NbTask task = tasks.iterator().next();
        assertTrue(controller.tasks.contains(task));
        
        // close the task externally
        assertFalse(task.isCompleted());
        TaskData external = task.getTaskDataState().getRepositoryData();
        TaskAttribute opAttr = external.getRoot().getMappedAttribute(TaskAttribute.OPERATION);
        TaskOperation taskOperation = null;
        for (TaskOperation op : external.getAttributeMapper().getTaskOperations(opAttr)) {
            if (BugzillaOperation.resolve.getLabel().equals(op.getLabel())) {
                taskOperation = op;
                break;
            }
        }
        assertNotNull(taskOperation);
        external.getAttributeMapper().setTaskOperation(opAttr, taskOperation);
        TaskAttribute resolutionAttr = external.getRoot().getMappedAttribute(BugzillaOperation.resolve.getInputId());
        resolutionAttr.setValue("WONTFIX");
        SubmitCommand submitCmd = new SubmitCommand(Bugzilla.getInstance().getRepositoryConnector(), btr, external);
        br.getExecutor().execute(submitCmd);
        
        // refresh query
        SynchronizeQueryCommand cmd = supp.getCommandFactory().createSynchronizeQueriesCommand(btr, q);
        cmd.addCommandProgressListener(controller);
        br.getExecutor().execute(cmd);
        
        // task should be removed from the list
        assertFalse(controller.getTasks().contains(task));
    }
    
    public void testSimpleSearch () throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        String queryName = "Temporary query";
        
        IRepositoryQuery q = supp.createNewQuery(btr, queryName);
        q.setUrl("/buglist.cgi?query_format=advanced&bug_id=1%2C2%2C3"); // three tasks
        // query list is empty
        assertFalse(supp.getRepositoryQueries(btr).contains(q));
        // it's still empty, need to sync first
        
        // synchronize
        SimpleQueryCommand cmd = supp.getCommandFactory().createSimpleQueryCommand(btr, q);
        br.getExecutor().execute(cmd);
                
        // get all tasks for the query
        Collection<NbTask> tasks = cmd.getTasks();
        assertEquals(3, tasks.size());
        
        Collection<NbTask> tasklistTasks = supp.getTasks(btr);
        // all tasks are in the tasklist
        for (NbTask t : tasks) {
            assertTrue(tasklistTasks.contains(t));
        }
        assertFalse(supp.getRepositoryQueries(btr).contains(q));
        
        // open tasks
        for (NbTask t : tasks) {
            DummyEditorPage p = new DummyEditorPage(t);
            p.open();
            p.waitUntilOpened();
            p.assertOpened();
            p.close();
        }
    }

    /**
     * This should be done in the editor page upon click on Submit
     */
    private NbTask submitTask (NbTask task, NbTaskDataModel model) throws CoreException {
        SubmitTaskCommand cmd = MylynSupport.getInstance().getCommandFactory().createSubmitTaskCommand(model);
        br.getExecutor().execute(cmd);
        NbTask newTask = cmd.getSubmittedTask();
        if (task == newTask) {
            // refresh model and whole editor page if opened
            model.refresh();
        }
        
        return newTask;
    }

    private void makeExternalChange (NbTask task, String newSummary) throws CoreException {
        MylynSupport supp = MylynSupport.getInstance();
        TaskData taskData = task.getTaskDataState().getRepositoryData();
        
        // edit the task externally
        TaskAttribute rta = taskData.getRoot();
        // now make an external change in summary
        TaskData external = new TaskData(taskData.getAttributeMapper(),
                taskData.getConnectorKind(),
                taskData.getRepositoryUrl(),
                taskData.getTaskId());
        external.setVersion(taskData.getVersion());
        for (TaskAttribute child : rta.getAttributes().values()) {
            external.getRoot().deepAddCopy(child);
        }
        external.getRoot().getMappedAttribute(TaskAttribute.SUMMARY).setValue(newSummary);
        SubmitCommand submitCmd = new SubmitCommand(Bugzilla.getInstance().getRepositoryConnector(), btr, external);
        br.getExecutor().execute(submitCmd);
    }

    private NbTask createNewTask (String summary) throws CoreException {
        MylynSupport supp = MylynSupport.getInstance();
        ITaskMapping mapping = new TaskMapping() {

            @Override
            public String getProduct () {
                return PRODUCT;
            }

            @Override
            public String getComponent () {
                return COMPONENT;
            }
            
        };
        NbTask task = supp.createTask(btr, mapping);
        NbTaskDataModel model = task.getTaskDataModel();
        
        // model.getTaskData returns our local data
        TaskAttribute rta = model.getLocalTaskData().getRoot();
        assertFalse(model.isDirty());
        // now edit summary, product and component
        String newSummary = summary;
        TaskAttribute ta = rta.getMappedAttribute(TaskAttribute.SUMMARY);
        ta.setValue(newSummary);
        model.attributeChanged(ta);
        
        // save
        model.save(new NullProgressMonitor());
        if (task.getSynchronizationState() == SynchronizationState.OUTGOING_NEW) {
            task.setSummary(newSummary);
        }
        return submitTask(task, model);
    }

    private void deleteTaskData (NbTask task) throws Exception {
        MylynSupport supp = MylynSupport.getInstance();
        Field f = MylynSupport.class.getDeclaredField("taskDataManager");
        f.setAccessible(true);
        TaskDataManager mgr = (TaskDataManager) f.get(supp);
        f = NbTask.class.getDeclaredField("delegate");
        f.setAccessible(true);
        mgr.deleteTaskData((ITask) f.get(task));
    }

    // something like BugzillaIssue
    private class DummyTaskWrapper implements NbTaskListener {
        private final NbTask task;
        private SynchronizationState syncState;
        private String summary;
        private String incomingChanges;

        public DummyTaskWrapper (NbTask task) {
            this.task = task;
            syncState = task.getSynchronizationState();
            summary = task.getSummary();
            task.addNbTaskListener(this);
        }

        // should be moved to a central place
        // maybe a BugzillaIssueManager
        @Override
        public void taskModified (TaskEvent ev) {
            if (ev.getTask() == task && ev.getKind() == TaskEvent.Kind.MODIFIED) {
                syncState = task.getSynchronizationState();
                summary = task.getSummary();
                incomingChanges = "";
                if (syncState == SynchronizationState.INCOMING
                        || syncState == SynchronizationState.CONFLICT) {
                    try {
                        NbTaskDataState taskDataState = task.getTaskDataState();
                        Set<TaskAttribute> changedAttributes = MylynSupport.getInstance().countDiff(
                                taskDataState.getRepositoryData(),
                                taskDataState.getLastReadData());
                        for (TaskAttribute changedAttr : changedAttributes) {
                            if (changedAttr.getId().equals(taskDataState.getRepositoryData().getRoot()
                                    .getMappedAttribute(TaskAttribute.SUMMARY).getId())) {
                                incomingChanges = "Summary from "
                                        + taskDataState.getLastReadData().getRoot().getMappedAttribute(TaskAttribute.SUMMARY).getValue()
                                        + " to "
                                        + taskDataState.getRepositoryData().getRoot().getMappedAttribute(TaskAttribute.SUMMARY).getValue();
                            }
                        }
                    } catch (CoreException ex) {
                        log(ex.toString());
                    }
                }
            }
        }
        
        void forget () {
            task.removeNbTaskListener(this);
        }

        private SynchronizationState getSynchronizationState () {
            return syncState;
        }

        private String getSummary () {
            return summary;
        }

        private String getIncomingChangesText () {
            return incomingChanges;
        }
    }

    private class DummyEditorPage implements TaskDataListener {
        private NbTask task;
        private String taskId;
        private NbTaskDataModel model;
        private String taskDataSummary;
        private boolean summaryChanged;
        private boolean summaryChangedLocally;
        private volatile boolean waitingToOpen;
        private MylynSupport supp;

        public DummyEditorPage (NbTask task) {
            this.task = task;
        }
        
        public DummyEditorPage (String taskId) {
            this.taskId = taskId;
        }
        
        void open () throws CoreException {
            supp = MylynSupport.getInstance();
            supp.addTaskDataListener(this);
            if (task == null || task.getTaskDataState() == null) {
                waitingToOpen = true;
                RequestProcessor.getDefault().schedule(new Runnable() {
                    @Override
                    public void run () {
                        if (waitingToOpen) {
                            try {
                                GetRepositoryTasksCommand cmd = supp.getCommandFactory().createGetRepositoryTasksCommand(
                                        btr, Collections.<String>singleton(task == null ? taskId : task.getTaskId()));
                                br.getExecutor().execute(cmd);
                                if (!cmd.getTasks().isEmpty()) {
                                    task = cmd.getTasks().iterator().next();
                                    if (task != null) {
                                        finishOpen();
                                    }
                                }
                            } catch (CoreException ex) {
                                log(ex.toString());
                            }
                        }
                    }
                }, 2, TimeUnit.SECONDS);
            } else {
                model = task.getTaskDataModel();
                if (model.getLocalTaskData().isPartial()) {
                    waitingToOpen = true;
                } else {
                    finishOpen();
                }
            }
        }
        
        void close () throws CoreException {
            save();
            MylynSupport.getInstance().removeTaskDataListener(this);
        }

        @Override
        public void taskDataUpdated (TaskDataEvent event) {
            if (event.getTask() == task) {
                if (event.getTaskData() != null && !event.getTaskData().isPartial() && waitingToOpen) {
                    try {
                        finishOpen();
                    } catch (CoreException ex) {
                        log(ex.getMessage());
                    }
                }
                if (event.getTaskDataUpdated()) {
                    refresh();
                }
            }
        }

        private void clear () {
            summaryChanged = summaryChangedLocally = false;
        }

        private void changeSummary (String newSummary) {
            TaskAttribute summaryAttr = model.getLocalTaskData().getRoot().getMappedAttribute(TaskAttribute.SUMMARY);
            summaryAttr.setValue(newSummary);
            model.attributeChanged(summaryAttr);
            assertTrue(model.isDirty());
        }

        private void save () throws CoreException {
            if (model.isDirty()) {
                model.save(null);
            }
        }

        private void revert () throws CoreException {
            task.discardLocalEdits();
            refresh();
        }

        private void refresh () {
            if (model == null) {
                return;
            }
            // maybe show a warning before overwriting the state
            try {
                model.refresh();
                TaskAttribute ta = model.getLocalTaskData().getRoot().getMappedAttribute(TaskAttribute.SUMMARY);
                summaryChangedLocally = model.hasOutgoingChanges(ta);
                summaryChanged = model.hasIncomingChanges(ta, true);
                taskDataSummary = ta.getValue();
            } catch (CoreException ex) {
                log(ex.toString());
            }
        }

        private void finishOpen () throws CoreException {
            if (model == null) {
                model = task.getTaskDataModel();
            }
            taskDataSummary = model.getLocalTaskData().getRoot().getMappedAttribute(TaskAttribute.SUMMARY).getValue();
            TaskAttribute ta = model.getLocalTaskData().getRoot().getMappedAttribute(TaskAttribute.SUMMARY);
            summaryChangedLocally = model.hasOutgoingChanges(ta);
            summaryChanged = model.hasIncomingChanges(ta, true);
            waitingToOpen = false;
        }
        
        private void assertOpened () {
            assertFalse(waitingToOpen);
        }

        private void closeTask (String resolution) {
            TaskOperation taskOperation = null;
            TaskAttribute opAttr = model.getLocalTaskData().getRoot().getMappedAttribute(TaskAttribute.OPERATION);
            for (TaskOperation op : model.getLocalTaskData().getAttributeMapper().getTaskOperations(opAttr)) {
                if (BugzillaOperation.resolve.getLabel().equals(op.getLabel())) {
                    taskOperation = op;
                    break;
                }
            }
            assertNotNull(taskOperation);
            assertFalse(task.isCompleted());
            model.getLocalTaskData().getAttributeMapper().setTaskOperation(opAttr, taskOperation);
            model.attributeChanged(opAttr);
            TaskAttribute resolutionAttr = model.getLocalTaskData().getRoot().getMappedAttribute(BugzillaOperation.resolve.getInputId());
            resolutionAttr.setValue(resolution);
            model.attributeChanged(resolutionAttr);
        }

        private void submit () throws CoreException {
            SubmitTaskCommand cmd = supp.getCommandFactory().createSubmitTaskCommand(model);
            br.getExecutor().execute(cmd);
        }

        private void waitUntilOpened () throws Exception {
            for (int i = 0; i < 50; ++i) {
                if (!waitingToOpen) {
                    break;
                }
                Thread.sleep(1000);
            }
            assertOpened();
        }
    }

    private class DummyQueryController implements SynchronizeQueryCommand.CommandProgressListener {
        private final IRepositoryQuery query;
        private final MylynSupport supp;
        private final Set<NbTask> tasks;
        private final List<DummyEditorPage> pages;
        private boolean flag = true;

        public DummyQueryController (IRepositoryQuery query) throws CoreException {
            this.query = query;
            this.supp = MylynSupport.getInstance();
            this.tasks = new HashSet<NbTask>(supp.getTasks(query));
            this.pages = new ArrayList<DummyEditorPage>();
        }

        @Override
        public void queryRefreshStarted (Collection<NbTask> tasks) {
            for (NbTask task : tasks) {
                taskAdded(task);
            }
        }

        @Override
        public void tasksRefreshStarted (Collection<NbTask> tasks) {
        }

        @Override
        public void taskAdded (NbTask task) {
            tasks.add(task);
            // open every other task to simulate fast clicking on task in query
            // when the task is not yet ready
            if (flag = !flag) {
                // open page
                DummyEditorPage page = new DummyEditorPage(task);
                pages.add(page);
                try {
                    page.open();
                } catch (CoreException ex) {
                    log(ex.getMessage());
                }
            }
        }

        @Override
        public void taskRemoved (NbTask task) {
            tasks.remove(task);
        }

        @Override
        public void taskSynchronized (NbTask task) {
        }

        void closeAllPages () throws CoreException {
            for (DummyEditorPage page : pages) {
                page.assertOpened();
                page.close();
            }
        }
        
        private Collection<NbTask> getTasks () {
            return tasks;
        }

        private Set<NbTask> getOpenedTasks () {
            Set<NbTask> retval = new HashSet<NbTask>(pages.size());
            for (DummyEditorPage p : pages) {
                retval.add(p.task);
            }
            return retval;
        }
    }
    
}
