/*******************************************************************************
 * Copyright (c) 2006, 2009 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
 *******************************************************************************/

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

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Random;

import junit.framework.TestCase;

import org.apache.commons.httpclient.HttpClient;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.eclipse.mylyn.internal.trac.core.util.TracHttpClientTransportFactory;
import org.eclipse.mylyn.tests.util.TestUtil;
import org.eclipse.mylyn.tests.util.TestUtil.Credentials;
import org.eclipse.mylyn.tests.util.TestUtil.PrivilegeLevel;
import org.eclipse.mylyn.trac.tests.support.TracTestConstants;

/**
 * Test cases for <a href="http://trac-hacks.org/wiki/XmlRpcPlugin">Trac XML-RPC Plugin</a> (revision 1188 or higher is
 * required).
 * <p>
 * This class does not depend on any Mylyn (connector) classes except for TracHttpClientTransportFactory which is needed
 * for initialization of HttpClient.
 * 
 * @author Steffen Pingel
 */
public class TracXmlRpcTest extends TestCase {

	public static final String XMLRPC_URL = "/login/xmlrpc";

	private XmlRpcClient xmlrpc;

	private String username;

	// private String password;

	private Random random;

	private ArrayList<Integer> tickets;

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

		random = new Random();

		Credentials credentials = TestUtil.readCredentials(PrivilegeLevel.ADMIN);
		createConnection(new URL(TracTestConstants.TEST_TRAC_010_URL + XMLRPC_URL), credentials.username,
				credentials.password);

		tickets = new ArrayList<Integer>();
	}

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

		for (int id : tickets) {
			call("ticket.delete", id);
		}
	}

	private void createConnection(URL url, String username, String password) throws Exception {
		XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
		config.setEncoding("UTF-8");
		config.setBasicUserName(username);
		config.setBasicPassword(password);
		config.setServerURL(url);

		xmlrpc = new XmlRpcClient();
		xmlrpc.setConfig(config);

		TracHttpClientTransportFactory factory = new TracHttpClientTransportFactory(xmlrpc, new HttpClient());
		xmlrpc.setTransportFactory(factory);

		this.username = username;
		// this.password = password;
	}

	private int createTicket(String summary, String description, Map<String, Object> attributes)
			throws XmlRpcException, IOException {
		int id = (Integer) call("ticket.create", summary, description, attributes);
		tickets.add(id);
		return id;
	}

	private Object call(String method, Object... parameters) throws XmlRpcException, IOException {
		Object result = xmlrpc.execute(method, parameters);
		if (result instanceof XmlRpcException) {
			throw (XmlRpcException) result;
		}
		return result;
	}

	public Map<String, Object> createMultiCall(String methodName, Object... parameters) throws XmlRpcException,
			IOException {
		Map<String, Object> table = new Hashtable<String, Object>();
		table.put("methodName", methodName);
		table.put("params", parameters);
		return table;
	}

	private void internalTestCrud(String module) throws XmlRpcException, IOException {
		try {
			call(module + ".delete", "foo");
		} catch (XmlRpcException e) {
		}

		call(module + ".create", "foo", "bar");
		try {
			assertHasValue((Object[]) call(module + ".getAll"), "foo");
			assertEquals("bar", (String) (call(module + ".get", "foo")));

			call(module + ".update", "foo", "baz");
			assertEquals("baz", (String) (call(module + ".get", "foo")));
		} finally {
			call(module + ".delete", "foo");
		}
	}

	private Object createValue(Object fieldName, Object clazz) {
		if (clazz == String.class) {
			return fieldName.toString() + random.nextInt();
		} else if (clazz == Date.class) {
			return new Date();
		} else if (clazz == Boolean.class) {
			return random.nextBoolean();
		} else if (clazz == Double.class) {
			return random.nextDouble();
		} else if (clazz == Integer.class) {
			return random.nextInt();
		}

		throw new RuntimeException("Invalid test data: " + fieldName + ", " + clazz);
	}

	private void internalTestComponent(String module, Object... fields) throws XmlRpcException, IOException {
		try {
			call(module + ".delete", "foo");
		} catch (XmlRpcException e) {
		}

		Map<String, Object> attributes = new Hashtable<String, Object>();
		for (int i = 0; i < fields.length; i += 2) {
			attributes.put((String) fields[i], createValue(fields[i], fields[i + 1]));
		}

		call(module + ".create", "foo", attributes);

		try {
			assertHasValue((Object[]) call(module + ".getAll"), "foo");
			Map<?, ?> values = (Map<?, ?>) call(module + ".get", "foo");
			for (String attribute : attributes.keySet()) {
				assertEquals(attributes.get(attribute), values.get(attribute));
			}

			for (int i = 0; i < fields.length; i += 2) {
				attributes.put((String) fields[i], createValue(fields[i], fields[i + 1]));
			}

			call(module + ".update", "foo", attributes);
			values = (Map<?, ?>) call(module + ".get", "foo");
			for (String attribute : attributes.keySet()) {
				assertEquals(attributes.get(attribute), values.get(attribute));
			}
		} finally {
			call(module + ".delete", "foo");
		}
	}

	public void testMilestoneDate() throws XmlRpcException, IOException {
		try {
			call("ticket.milestone.delete", "foo");
		} catch (XmlRpcException e) {
		}

		int due = (int) (System.currentTimeMillis() / 1000) + 1000;
		int completed = (int) (System.currentTimeMillis() / 1000);

		Map<String, Object> attributes = new Hashtable<String, Object>();
		attributes.put("description", "description");
		attributes.put("due", due);
		attributes.put("completed", completed);

		call("ticket.milestone.create", "foo", attributes);

		Map<?, ?> values = (Map<?, ?>) call("ticket.milestone.get", "foo");
		assertEquals(new Integer(due), values.get("due"));
		assertEquals(new Integer(completed), values.get("completed"));

		call("ticket.milestone.delete", "foo");
	}

	private void assertHasValue(Object[] items, Object value) {
		for (Object item : items) {
			if (item.equals(value)) {
				return;
			}
		}
		fail("Could not find expected value: " + value);
	}

	private void assertTicketHasAttributes(Map<String, Object> attributes, int id, Object[] ticket) {
		assertTicketHasAttributes(attributes, id, ticket, true);
	}

	private void assertTicketHasAttributes(Map<String, Object> attributes, int id, Object[] ticket, boolean newTicket) {
		assertEquals(id, ticket[0]);
		assertTrue(ticket[1] instanceof Integer); // time created
		// time changed
		if (newTicket) {
			assertEquals(ticket[1], ticket[2]);
		} else {
			assertTrue((Integer) ticket[2] >= (Integer) ticket[1]);
		}
		Map<?, ?> values = (Map<?, ?>) ticket[3];
		for (String attribute : attributes.keySet()) {
			assertEquals(attributes.get(attribute), values.get(attribute));
		}
	}

	public void testGetTicket() throws XmlRpcException, IOException {
		Map<String, Object> attributes = new Hashtable<String, Object>();
		attributes.put("type", "task");
		attributes.put("status", "closed");
		int id = createTicket("summary", "description", attributes);

		attributes.put("summary", "summary");
		attributes.put("description", "description");

		Object[] ticket = (Object[]) call("ticket.get", id);
		assertTicketHasAttributes(attributes, id, ticket);
	}

	public void testGetTicketNonExistant() throws XmlRpcException, IOException {
		try {
			call("ticket.delete", Integer.MAX_VALUE);
		} catch (Exception e) {
			// ignore
		}

		try {
			List<?> ticket = (List<?>) call("ticket.get", Integer.MAX_VALUE);
			fail("Expected XmlRpcException, got ticket instead: " + ticket);
		} catch (XmlRpcException e) {
			// ignore
		}
	}

	public void testGetTicketUmlaute() throws XmlRpcException, IOException {
		Map<String, Object> attributes = new Hashtable<String, Object>();
		int id = createTicket("summarya\u0308O\u030b", "\u00d8", attributes);

		attributes.put("summary", "summarya\u0308O\u030b");
		attributes.put("description", "\u00d8");

		Object[] ticket = (Object[]) call("ticket.get", id);
		assertTicketHasAttributes(attributes, id, ticket);
	}

	public void testUpdateTicket() throws XmlRpcException, IOException {
		int id = createTicket("summary", "description", new Hashtable<String, Object>());

		Map<String, Object> attributes = new Hashtable<String, Object>();
		attributes.put("summary", "changed");
		call("ticket.update", id, "my comment", attributes);

		attributes.put("description", "description");

		Object[] ticket = (Object[]) call("ticket.get", id);
		Map<?, ?> values = (Map<?, ?>) ticket[3];
		for (String attribute : attributes.keySet()) {
			assertEquals(attributes.get(attribute), values.get(attribute));
		}
	}

	public void testTicketCustomFields() throws XmlRpcException, IOException {
		Map<String, Object> attributes = new Hashtable<String, Object>();
		attributes.put("custom_text_field", "myvalue");
		int id = createTicket("summary", "description", attributes);

		// check for default values
		attributes.put("custom_checkbox_field", "1");
		attributes.put("custom_select_field", "two");
		attributes.put("custom_radio_field", "baz");
		attributes.put("custom_textarea_field", "default text");

		Object[] ticket = (Object[]) call("ticket.get", id);
		assertTicketHasAttributes(attributes, id, ticket);

		attributes.put("custom_text_field", "myvalue2");
		attributes.put("custom_checkbox_field", "0");
		attributes.put("custom_select_field", "one");
		attributes.put("custom_radio_field", "foo");
		attributes.put("custom_textarea_field", "mytext");

		call("ticket.update", id, "my comment", attributes);

		ticket = (Object[]) call("ticket.get", id);
		assertTicketHasAttributes(attributes, id, ticket, false);
	}

	public void testGetChangeLog() throws XmlRpcException, IOException {
		int id = createTicket("summary", "description", new Hashtable<String, Object>());

		Map<String, Object> attributes = new Hashtable<String, Object>();
		attributes.put("summary", "changed");
		call("ticket.update", id, "my comment", attributes);

		Object[] log = (Object[]) call("ticket.changeLog", id, 0);
		Object[] entry = (Object[]) log[0];
		assertTrue(entry[0] instanceof Integer); // time
		assertEquals(username, entry[1]); // author
		assertEquals("summary", entry[2]); // field
		assertEquals("summary", entry[3]); // old value
		assertEquals("changed", entry[4]); // new value
	}

	public void testMultiGetTicket() throws XmlRpcException, IOException {
		int id1 = createTicket("summary1", "description1", new Hashtable<String, Object>());
		int id2 = createTicket("summary2", "description2", new Hashtable<String, Object>());

		List<Map<?, ?>> calls = new ArrayList<Map<?, ?>>();
		calls.add(createMultiCall("ticket.get", id1));
		calls.add(createMultiCall("ticket.get", id2));
		Object[] ret = (Object[]) call("system.multicall", calls);

		Object[] ticket = (Object[]) ((Object[]) ret[0])[0];
		Map<String, Object> attributes = new Hashtable<String, Object>();
		attributes.put("summary", "summary1");
		attributes.put("description", "description1");
		assertTicketHasAttributes(attributes, id1, ticket);

		ticket = (Object[]) ((Object[]) ret[1])[0];
		attributes.clear();
		attributes.put("summary", "summary2");
		attributes.put("description", "description2");
		assertTicketHasAttributes(attributes, id2, ticket);
	}

	public void testAttachment() throws XmlRpcException, IOException {
		int id = createTicket("summary", "description", new Hashtable<String, Object>());

		String filename = (String) call("ticket.putAttachment", id, "attach.txt", "description", "data".getBytes(),
				true);
		// the returned filename may differ, since another ticket may have an
		// attachment named "attach.txt"
		// assertEquals("attach.txt", filename);

		Object[] ret = (Object[]) call("ticket.listAttachments", id);
		assertEquals(1, ret.length);
		Object[] attachment = (Object[]) ret[0];
		assertEquals("attach.txt", attachment[0]);
		assertEquals("description", attachment[1]);
		assertEquals(4, attachment[2]);
		// date
		assertEquals(username, attachment[4]);

		byte[] bytes = (byte[]) call("ticket.getAttachment", id, filename);
		String data = new String(bytes);
		assertEquals("data", data);

		// test override

		String filename2 = (String) call("ticket.putAttachment", id, filename, "newdescription", "newdata".getBytes(),
				true);
		assertEquals(filename, filename2);
		ret = (Object[]) call("ticket.listAttachments", id);
		assertEquals(1, ret.length);
		attachment = (Object[]) ret[0];
		assertEquals("attach.txt", attachment[0]);
		assertEquals("newdescription", attachment[1]);
		assertEquals(7, attachment[2]);
		// date
		assertEquals(username, attachment[4]);
		bytes = (byte[]) call("ticket.getAttachment", id, filename);
		data = new String(bytes);
		assertEquals("newdata", data);

		String filename3 = (String) call("ticket.putAttachment", id, "attach.txt", "description", "data".getBytes(),
				false);
		assertFalse("attach.txt".equals(filename3));
		ret = (Object[]) call("ticket.listAttachments", id);
		assertEquals(2, ret.length);
	}

	public void testDeleteAttachment() throws XmlRpcException, IOException {
		int id = createTicket("summary", "description", new Hashtable<String, Object>());

		String filename = (String) call("ticket.putAttachment", id, "attach.txt", "description", "data".getBytes(),
				true);

		Object[] ret = (Object[]) call("ticket.listAttachments", id);
		assertEquals(1, ret.length);

		call("ticket.deleteAttachment", id, filename);

		ret = (Object[]) call("ticket.listAttachments", id);
		assertEquals(0, ret.length);
	}

	public void testDuplicateAttachment() throws XmlRpcException, IOException {
		int id1 = createTicket("summary", "description", new Hashtable<String, Object>());
		int id2 = createTicket("summary", "description", new Hashtable<String, Object>());

		String filename1 = (String) call("ticket.putAttachment", id1, "attach.txt", "description", "data".getBytes(),
				true);
		String filename2 = (String) call("ticket.putAttachment", id2, "attach.txt", "description", "data2".getBytes(),
				true);
		assertEquals("attach.txt", filename1);
		assertEquals(filename1, filename2);

		byte[] bytes = (byte[]) call("ticket.getAttachment", id1, "attach.txt");
		String data = new String(bytes);
		assertEquals("data", data);

		bytes = (byte[]) call("ticket.getAttachment", id2, "attach.txt");
		data = new String(bytes);
		assertEquals("data2", data);
	}

	public void testQuery() throws XmlRpcException, IOException {
		Object[] ret = (Object[]) call("ticket.query", "summary~=foo|bar|baz");
		for (Object id : ret) {
			call("ticket.delete", (Integer) id);
		}

		int id1 = createTicket("foobarsum1", "description", new Hashtable<String, Object>());
		int id2 = createTicket("foobaz sum2", "description", new Hashtable<String, Object>());
		int id3 = createTicket("foobarbaz3", "foobarbaz description3", new Hashtable<String, Object>());

		ret = (Object[]) call("ticket.query", "summary=foobarsum1|foobaz sum2");
		assertEquals(2, ret.length);
		assertEquals(id1, ret[0]);
		assertEquals(id2, ret[1]);

		// the first criterium is ignored
		ret = (Object[]) call("ticket.query", "summary~=foobarsum1&summary~=foobaz sum2");
		assertEquals(1, ret.length);
		assertEquals(id2, ret[0]);

		ret = (Object[]) call("ticket.query", "summary~=bar|baz");
		assertEquals(3, ret.length);

		ret = (Object[]) call("ticket.query", "description~=foobarbaz description3");
		assertEquals(1, ret.length);
		assertEquals(id3, ret[0]);
	}

	public void testQueryAll() throws XmlRpcException, IOException {
		int id = createTicket("foo", "description", new Hashtable<String, Object>());

		Object[] ret = (Object[]) call("ticket.query", "order=id");
		assertTrue(ret.length > 0);
		assertHasValue(ret, id);
	}

	public void testPriorities() throws XmlRpcException, IOException {
		internalTestCrud("ticket.priority");
	}

	public void testSeverities() throws XmlRpcException, IOException {
		internalTestCrud("ticket.severity");
	}

	public void testType() throws XmlRpcException, IOException {
		internalTestCrud("ticket.type");
	}

	public void testStatus() throws XmlRpcException, IOException {
		internalTestCrud("ticket.status");
	}

	public void testResolutions() throws XmlRpcException, IOException {
		internalTestCrud("ticket.resolution");
	}

	public void testVersions() throws XmlRpcException, IOException {
		internalTestComponent("ticket.version", "time", Integer.class, "description", String.class);
	}

	public void testComponents() throws XmlRpcException, IOException {
		internalTestComponent("ticket.component", "owner", String.class, "description", String.class);
	}

	public void testMilestones() throws XmlRpcException, IOException {
		internalTestComponent("ticket.milestone", "due", Integer.class, "completed", Integer.class, "description",
				String.class);
	}

}
