/*******************************************************************************
 * Copyright (c) 2006, 2012 Steffen Pingel and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Steffen Pingel - initial API and implementation
 *     Benjamin Muskalla - bug 386920
 *******************************************************************************/

package org.eclipse.mylyn.trac.tests.core;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;

import junit.framework.TestCase;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.mylyn.commons.net.AuthenticationCredentials;
import org.eclipse.mylyn.commons.net.AuthenticationType;
import org.eclipse.mylyn.internal.tasks.core.TaskTask;
import org.eclipse.mylyn.internal.tasks.core.data.TextTaskAttachmentSource;
import org.eclipse.mylyn.internal.tasks.core.sync.SynchronizationSession;
import org.eclipse.mylyn.internal.trac.core.TracAttribute;
import org.eclipse.mylyn.internal.trac.core.TracAttributeMapper;
import org.eclipse.mylyn.internal.trac.core.TracCorePlugin;
import org.eclipse.mylyn.internal.trac.core.TracRepositoryConnector;
import org.eclipse.mylyn.internal.trac.core.TracTaskDataHandler;
import org.eclipse.mylyn.internal.trac.core.TracTaskMapper;
import org.eclipse.mylyn.internal.trac.core.client.ITracClient;
import org.eclipse.mylyn.internal.trac.core.model.TracTicket;
import org.eclipse.mylyn.internal.trac.core.model.TracTicket.Key;
import org.eclipse.mylyn.internal.trac.core.util.TracUtil;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.ITaskAttachment;
import org.eclipse.mylyn.tasks.core.RepositoryStatus;
import org.eclipse.mylyn.tasks.core.TaskMapping;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskAttachmentHandler;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
import org.eclipse.mylyn.tasks.core.data.TaskAttributeMapper;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.core.data.TaskMapper;
import org.eclipse.mylyn.tasks.core.data.TaskOperation;
import org.eclipse.mylyn.tasks.core.data.TaskRelation;
import org.eclipse.mylyn.trac.tests.support.TracFixture;
import org.eclipse.mylyn.trac.tests.support.TracHarness;
import org.eclipse.mylyn.trac.tests.support.TracTestUtil;

/**
 * @author Steffen Pingel
 * @author Benjamin Muskalla
 */
public class TracTaskDataHandlerXmlRpcTest extends TestCase {

	private TracRepositoryConnector connector;

	private TaskRepository repository;

	private TracTaskDataHandler taskDataHandler;

	private ITracClient client;

	private TracHarness harness;

	public TracTaskDataHandlerXmlRpcTest() {
	}

	@Override
	protected void setUp() throws Exception {
		harness = TracFixture.current().createHarness();
		connector = harness.connector();
		taskDataHandler = connector.getTaskDataHandler();
		repository = harness.repository();
		client = connector.getClientManager().getTracClient(repository);
	}

	@Override
	protected void tearDown() throws Exception {
		harness.dispose();
	}

	private SynchronizationSession createSession(ITask... tasks) {
		SynchronizationSession session = new SynchronizationSession();
		session.setNeedsPerformQueries(true);
		session.setTaskRepository(repository);
		session.setFullSynchronization(true);
		session.setTasks(new HashSet<ITask>(Arrays.asList(tasks)));
		return session;
	}

	public void testMarkStaleTasks() throws Exception {
		SynchronizationSession session;
		// sleep for one second to ensure that the created ticket has a unique time stamp
		Thread.sleep(1000);
		TracTicket ticket = harness.createTicket("markStaleTasks");
		ITask task = harness.getTask(ticket);
		long lastModified = TracUtil.toTracTime(task.getModificationDate());

		// an empty set should not cause contact to the repository
		repository.setSynchronizationTimeStamp(null);
		session = createSession(task);
		connector.preSynchronization(session, null);
		assertTrue(session.needsPerformQueries());
		assertNull(repository.getSynchronizationTimeStamp());

		repository.setSynchronizationTimeStamp(null);
		session = createSession(task);
		connector.preSynchronization(session, null);
		assertTrue(session.needsPerformQueries());
		assertEquals(Collections.singleton(task), session.getStaleTasks());

		// always returns the ticket because time comparison mode is >=
		repository.setSynchronizationTimeStamp(lastModified + "");
		session = createSession(task);
		connector.preSynchronization(session, null);
		// false since query that check for changed tasks only returns a single task
		assertFalse(session.needsPerformQueries());
		assertEquals(Collections.emptySet(), session.getStaleTasks());

		// nothing has changed, should not detect a change
		repository.setSynchronizationTimeStamp((lastModified + 1) + "");
		session = createSession(task);
		connector.preSynchronization(session, null);
		assertFalse(session.needsPerformQueries());
		assertEquals(Collections.emptySet(), session.getStaleTasks());

		long mostRecentlyModified = 0;
		// try changing ticket 3x to make sure it gets a new change time
		for (int i = 0; i < 3; i++) {
			ticket = client.getTicket(ticket.getId(), null);
			ticket.putBuiltinValue(Key.DESCRIPTION, lastModified + "");
			client.updateTicket(ticket, "comment", null);
			mostRecentlyModified = TracUtil.toTracTime(ticket.getLastChanged());
			// needs to be at least one second ahead of repository time stamp   
			if (mostRecentlyModified >= lastModified + 1) {
				break;
			} else if (i == 2) {
				fail("Failed to update ticket modification time: ticket id=" + ticket.getId() + ", lastModified="
						+ lastModified + ", mostRectentlyModified=" + mostRecentlyModified);
			}
			Thread.sleep(1500);
		}

		// should now detect a change
		repository.setSynchronizationTimeStamp((lastModified + 1) + "");
		session = createSession(task);
		connector.preSynchronization(session, null);
		assertTrue("Expected change: ticket id=" + ticket.getId() + ", lastModified=" + lastModified
				+ ", mostRectentlyModified=" + mostRecentlyModified, session.needsPerformQueries());
		assertEquals(Collections.singleton(task), session.getStaleTasks());
	}

	public void testMarkStaleTasksNoTimeStamp() throws Exception {
		ITask task = harness.createTask("MarkStaleTasksNoTimeStamp");

		SynchronizationSession session = createSession(task);
		repository.setSynchronizationTimeStamp(null);
		connector.preSynchronization(session, null);
		assertTrue(session.needsPerformQueries());
		assertEquals(Collections.singleton(task), session.getStaleTasks());

		session = createSession(task);
		repository.setSynchronizationTimeStamp("");
		connector.preSynchronization(session, null);
		assertTrue(session.needsPerformQueries());
		assertEquals(Collections.singleton(task), session.getStaleTasks());

		session = createSession(task);
		repository.setSynchronizationTimeStamp("0");
		connector.preSynchronization(session, null);
		assertTrue(session.needsPerformQueries());
		assertEquals(Collections.singleton(task), session.getStaleTasks());

		session = createSession(task);
		repository.setSynchronizationTimeStamp("abc");
		connector.preSynchronization(session, null);
		assertTrue(session.needsPerformQueries());
		assertEquals(Collections.singleton(task), session.getStaleTasks());
	}

	public void testNonNumericTaskId() {
		try {
			connector.getTaskData(repository, "abc", null);
			fail("Expected CoreException");
		} catch (CoreException e) {
		}
	}

	public void testAttachmentChangesLastModifiedDate() throws Exception {
		AbstractTaskAttachmentHandler attachmentHandler = connector.getTaskAttachmentHandler();
		ITask task = harness.createTask("attachmentChangesLastModifiedDate");
		Date lastModified = task.getModificationDate();
		harness.getFixture().waitToGuaranteeTaskUpdate();
		// XXX the test case fails when comment == null
		attachmentHandler.postContent(repository, task, new TextTaskAttachmentSource("abc"), "comment", null, null);

		task = harness.getTask(task.getTaskId());
		Date newLastModified = task.getModificationDate();
		assertTrue("Expected " + newLastModified + " to be more recent than " + lastModified,
				newLastModified.after(lastModified));
	}

	public void testAttachmentUrlEncoding() throws Exception {
		AbstractTaskAttachmentHandler attachmentHandler = connector.getTaskAttachmentHandler();
		TracTicket ticket = harness.createTicket("attachment url test");

		ITask task = harness.getTask(ticket);
		attachmentHandler.postContent(repository, task, new TextTaskAttachmentSource("abc") {
			@Override
			public String getName() {
				return "https%3A%2F%2Fbugs.eclipse.org%2Fbugs.xml.zip";
			}
		}, "comment", null, null);

		task = harness.getTask(ticket);
		List<ITaskAttachment> attachments = TracTestUtil.getTaskAttachments(task);
		assertEquals(1, attachments.size());
		assertEquals(repository.getUrl() + "/attachment/ticket/" + ticket.getId()
				+ "/https%253A%252F%252Fbugs.eclipse.org%252Fbugs.xml.zip", attachments.get(0).getUrl());
	}

	public void testPostTaskDataInvalidCredentials() throws Exception {
		TaskData taskData = harness.createTaskData("postTaskDataInvalidCredentials");
		taskData.getRoot().getMappedAttribute(TaskAttribute.COMMENT_NEW).setValue("new comment");
		repository.setCredentials(AuthenticationType.REPOSITORY, new AuthenticationCredentials("foo", "bar"), false);
		try {
			taskDataHandler.postTaskData(repository, taskData, null, null);
		} catch (CoreException expected) {
			assertEquals(RepositoryStatus.ERROR_REPOSITORY_LOGIN, expected.getStatus().getCode());
		}
		assertEquals("new comment", taskData.getRoot().getMappedAttribute(TaskAttribute.COMMENT_NEW).getValue());
	}

	public void testCanInitializeTaskData() throws Exception {
		ITask task = new TaskTask(TracCorePlugin.CONNECTOR_KIND, "", "");
		assertFalse(taskDataHandler.canInitializeSubTaskData(repository, task));
		task.setAttribute(TracRepositoryConnector.TASK_KEY_SUPPORTS_SUBTASKS, Boolean.TRUE.toString());
		assertTrue(taskDataHandler.canInitializeSubTaskData(repository, task));
	}

	public void testCanInitializeTaskDataRepositoryTask() throws Exception {
		ITask task = harness.createTask("canInitializeTaskDataRepositoryTask");
		TaskData taskData = taskDataHandler.getTaskData(repository, task.getTaskId(), null);
		assertFalse(taskDataHandler.canInitializeSubTaskData(repository, task));

		taskData.getRoot().createAttribute(TracTaskDataHandler.ATTRIBUTE_BLOCKED_BY);
		connector.updateTaskFromTaskData(repository, task, taskData);
		assertTrue(taskDataHandler.canInitializeSubTaskData(repository, task));

		task.setAttribute(TracRepositoryConnector.TASK_KEY_SUPPORTS_SUBTASKS, Boolean.FALSE.toString());
		connector.updateTaskFromTaskData(repository, task, taskData);
		assertTrue(taskDataHandler.canInitializeSubTaskData(repository, task));
	}

	public void testInitializeSubTaskDataInvalidParent() throws Exception {
		TaskData parentTaskData = harness.createTaskData("initializeSubTaskDataInvalidParent");
		try {
			taskDataHandler.initializeSubTaskData(repository, parentTaskData, parentTaskData, null);
			fail("expected CoreException");
		} catch (CoreException expected) {
		}
	}

	public void testInitializeSubTaskData() throws Exception {
		TaskData parentTaskData = harness.createTaskData("initializeSubTaskData");
		TaskMapper parentTaskMapper = new TracTaskMapper(parentTaskData, null);
		parentTaskMapper.setSummary("abc");
		parentTaskMapper.setDescription("def");
		String component = parentTaskData.getRoot()
				.getMappedAttribute(TracAttribute.COMPONENT.getTracKey())
				.getOptions()
				.get(0);
		parentTaskMapper.setComponent(component);
		parentTaskData.getRoot().createAttribute(TracTaskDataHandler.ATTRIBUTE_BLOCKED_BY);
		TaskData subTaskData = new TaskData(parentTaskData.getAttributeMapper(), TracCorePlugin.CONNECTOR_KIND, "", "");
		subTaskData.getRoot().createAttribute(TracTaskDataHandler.ATTRIBUTE_BLOCKING);
		taskDataHandler.initializeSubTaskData(repository, subTaskData, parentTaskData, new NullProgressMonitor());
		TaskMapper subTaskMapper = new TracTaskMapper(subTaskData, null);
		assertEquals("", subTaskMapper.getSummary());
		assertEquals("", subTaskMapper.getDescription());
		assertEquals(component, subTaskMapper.getComponent());
		TaskAttribute attribute = subTaskData.getRoot().getMappedAttribute(TracTaskDataHandler.ATTRIBUTE_BLOCKING);
		assertEquals(parentTaskData.getTaskId(), attribute.getValue());
		attribute = parentTaskData.getRoot().getMappedAttribute(TracTaskDataHandler.ATTRIBUTE_BLOCKED_BY);
		assertEquals("", attribute.getValue());
	}

	public void testGetSubTaskIds() throws Exception {
		TaskData taskData = new TaskData(new TracAttributeMapper(new TaskRepository("", ""), client),
				TracCorePlugin.CONNECTOR_KIND, "", "");
		TaskAttribute blockedBy = taskData.getRoot().createAttribute(TracTaskDataHandler.ATTRIBUTE_BLOCKED_BY);
		Collection<String> subTaskIds;

		blockedBy.setValue("123 456");
		subTaskIds = getSubTaskIds(taskData);
		assertEquals(2, subTaskIds.size());
		assertTrue(subTaskIds.contains("123"));
		assertTrue(subTaskIds.contains("456"));

		blockedBy.setValue("7,8");
		subTaskIds = getSubTaskIds(taskData);
		assertEquals(2, subTaskIds.size());
		assertTrue(subTaskIds.contains("7"));
		assertTrue(subTaskIds.contains("8"));

		blockedBy.setValue("  7 ,   8,  ");
		subTaskIds = getSubTaskIds(taskData);
		assertEquals(2, subTaskIds.size());
		assertTrue(subTaskIds.contains("7"));
		assertTrue(subTaskIds.contains("8"));

		blockedBy.setValue("7");
		subTaskIds = getSubTaskIds(taskData);
		assertEquals(1, subTaskIds.size());
		assertTrue(subTaskIds.contains("7"));

		blockedBy.setValue("");
		subTaskIds = getSubTaskIds(taskData);
		assertEquals(0, subTaskIds.size());

		blockedBy.setValue("  ");
		subTaskIds = getSubTaskIds(taskData);
		assertEquals(0, subTaskIds.size());
	}

	private Collection<String> getSubTaskIds(TaskData taskData) {
		List<String> subTaskIds = new ArrayList<String>();
		Collection<TaskRelation> relations = connector.getTaskRelations(taskData);
		for (TaskRelation taskRelation : relations) {
			subTaskIds.add(taskRelation.getTaskId());
		}
		return subTaskIds;
	}

	public void testInitializeTaskData() throws Exception {
		TaskData taskData = new TaskData(taskDataHandler.getAttributeMapper(repository), TracCorePlugin.CONNECTOR_KIND,
				"", "");
		TaskMapping mapping = new TaskMapping() {
			@Override
			public String getDescription() {
				return "description";
			}

			@Override
			public String getSummary() {
				return "summary";
			}
		};
		taskDataHandler.initializeTaskData(repository, taskData, mapping, new NullProgressMonitor());
		// initializeTaskData() should ignore the initialization data 
		TaskMapper mapper = new TracTaskMapper(taskData, null);
		assertEquals(null, mapper.getResolution());
		assertEquals("", mapper.getSummary());
		assertEquals("", mapper.getDescription());
		// check for default values
		assertEquals("Defect", mapper.getTaskKind());
		assertEquals("major", mapper.getPriority());
		// empty attributes should not exist
		assertNull(taskData.getRoot().getAttribute(TracAttribute.SEVERITY.getTracKey()));
	}

	public void testInitializeTaskDataNoMonitor() throws Exception {
		connector.getClientManager().repositoryRemoved(repository);
		TaskData taskData = new TaskData(new TaskAttributeMapper(repository), TracCorePlugin.CONNECTOR_KIND, "", "");
		boolean initialized = taskDataHandler.initializeTaskData(repository, taskData, new TaskMapping(), null);
		assertTrue(initialized);
	}

	public void testOperations() throws Exception {
		boolean hasReassign = TracFixture.current().getVersion().compareTo("0.11") >= 0;

		TaskData taskData = taskDataHandler.getTaskData(repository, "1", new NullProgressMonitor());
		List<TaskAttribute> operations = taskData.getAttributeMapper().getAttributesByType(taskData,
				TaskAttribute.TYPE_OPERATION);
		assertEquals("Unexpected operations: " + operations, (hasReassign ? 5 : 4), operations.size());

		TaskOperation operation = taskData.getAttributeMapper().getTaskOperation(operations.get(0));
		assertEquals(TaskAttribute.OPERATION, operation.getTaskAttribute().getId());

		operation = taskData.getAttributeMapper().getTaskOperation(operations.get(1));
		assertEquals("leave", operation.getOperationId());
		assertNotNull(operation.getLabel());

		operation = taskData.getAttributeMapper().getTaskOperation(operations.get(2));
		assertEquals("resolve", operation.getOperationId());
		assertNotNull(operation.getLabel());
		String associatedId = operation.getTaskAttribute()
				.getMetaData()
				.getValue(TaskAttribute.META_ASSOCIATED_ATTRIBUTE_ID);
		assertNotNull(associatedId);

		if (hasReassign) {
			operation = taskData.getAttributeMapper().getTaskOperation(operations.get(3));
			assertEquals("reassign", operation.getOperationId());
			assertNotNull(operation.getLabel());

			operation = taskData.getAttributeMapper().getTaskOperation(operations.get(4));
			assertEquals("accept", operation.getOperationId());
			assertNotNull(operation.getLabel());
		} else {
			operation = taskData.getAttributeMapper().getTaskOperation(operations.get(3));
			assertEquals("accept", operation.getOperationId());
			assertNotNull(operation.getLabel());
		}
	}

	public void testPostTaskDataUnsetResolution() throws Exception {
		TracTicket ticket = harness.createTicket("postTaskDataUnsetResolution");
		TaskData taskData = taskDataHandler.getTaskData(repository, ticket.getId() + "", new NullProgressMonitor());
		TaskAttribute attribute = taskData.getRoot().getMappedAttribute(TaskAttribute.RESOLUTION);
		attribute.setValue("fixed");
		taskDataHandler.postTaskData(repository, taskData, null, new NullProgressMonitor());

		// should not set resolution unless resolve operation is selected
		taskData = taskDataHandler.getTaskData(repository, ticket.getId() + "", new NullProgressMonitor());
		attribute = taskData.getRoot().getMappedAttribute(TaskAttribute.RESOLUTION);
		assertEquals("", attribute.getValue());
	}

	public void testPostTaskDataMidAirCollision() throws Exception {
		TracTicket ticket = harness.createTicket("midAirCollision");
		if (ticket.getValue(Key.TOKEN) == null) {
			// repository does not have mid-air collision support
			System.err.println("Skipping TracTaskDataHandler.testPostTaskDataMidAirCollision() due to lack of mid-air collision support on "
					+ repository.getRepositoryUrl());
			return;
		}
		TaskData taskData = taskDataHandler.getTaskData(repository, ticket.getId() + "", new NullProgressMonitor());
		TaskAttribute attribute = taskData.getRoot().getMappedAttribute(TaskAttribute.PRIORITY);
		attribute.setValue("blocker");

		// change ticket in repository
		ticket.putBuiltinValue(Key.PRIORITY, "trivial");
		client.updateTicket(ticket, "changing priority", null);

		harness.getFixture().waitToGuaranteeTaskUpdate();

		// submit conflicting change 
		try {
			taskDataHandler.postTaskData(repository, taskData, null, new NullProgressMonitor());
			fail("Expected CoreException due to mid-air collision");
		} catch (CoreException e) {
			assertEquals(RepositoryStatus.createCollisionError(repository.getRepositoryUrl(), TracCorePlugin.ID_PLUGIN)
					.getMessage(), e.getStatus().getMessage());
		}
	}

}
