package org.archive.wayback.archivalurl;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.http.HttpServletRequest;

import junit.framework.TestCase;

import org.archive.wayback.archivalurl.requestparser.DatelessReplayRequestParser;
import org.archive.wayback.archivalurl.requestparser.PathDatePrefixQueryRequestParser;
import org.archive.wayback.archivalurl.requestparser.PathDateRangeQueryRequestParser;
import org.archive.wayback.archivalurl.requestparser.PathPrefixDatePrefixQueryRequestParser;
import org.archive.wayback.archivalurl.requestparser.PathPrefixDateRangeQueryRequestParser;
import org.archive.wayback.archivalurl.requestparser.ReplayRequestParser;
import org.archive.wayback.core.WaybackRequest;
import org.archive.wayback.exception.BadQueryException;
import org.archive.wayback.exception.BetterRequestException;
import org.archive.wayback.memento.MementoConstants;
import org.archive.wayback.memento.TimeGateBadQueryException;
import org.archive.wayback.memento.TimeMapRequestParser;
import org.archive.wayback.webapp.AccessPoint;
import org.easymock.EasyMock;
import org.easymock.IAnswer;

/**
 * Test case for {@link ArchivalUrlRequestParser}.
 *
 * This class tests entire {@code ArchivalUrlRequestParser}, including its sub-component
 * classes: {@link ReplayRequestParser}, {@link PathDatePrefixQueryRequestParser}
 * {@link PathDateRangeQueryRequestParser}, {@link PathPrefixDatePrefixQueryRequestParser},
 * {@link PathPrefixDateRangeQueryRequestParser} and {@link DatelessReplayRequestParser}.
 * 
 * @author Kenji Nagahashi
 *
 */
public class ArchivalUrlRequestParserTest extends TestCase {
	ArchivalUrlRequestParser cut;
	AccessPoint accessPoint;

	// must set before calling parse().
	String acceptTimestampHeader = null;
	String acceptDatetimeHeader = null;

	final String EARLIEST_TIMESTAMP = "1996";
	final String EXPECTED_START_TIMESTAMP = "19960101000000";
	final String EXPECTED_END_TIMESTAMP = null;

	protected void setUp() throws Exception {
		super.setUp();
		cut = new ArchivalUrlRequestParser();
		// this affects what WaybackRequest.getStartTimestamp() returns.
		// here we leave lastestTimestamp null.
		cut.setEarliestTimestamp(EARLIEST_TIMESTAMP);
		// this needs to be called before use (but I don't see it called in
		// Spring config.)
		cut.init();
		// refactoring note: RequestParser#parser takes AccessPoint as an argument.
		// it is bad because AccessPoint is not mock-able and it is a big class.
		// it'd be great if we can cut out an interface required for RequestParser
		// and use it in RequestParser instead.
		// (partial) list of AccessPoint methods used by RequestParser:
		// - translateRequestPathQuery(HttpServletRequest) (defined in RequestHandler)
		// - getMapParam(Map, String) (static)
		// - isEnableMemento()
		accessPoint = new AccessPoint();
	}
	// lame query parameter parser just sufficient for testing.
	// FIXME: replace this with exiting well-tested library if there's one.
	private Map<String, String[]> parseParameters(String url) {
		int iq = url.indexOf("?");
		if (iq < 0)
			return Collections.emptyMap();

		Map<String, List<String>> map = new HashMap<String, List<String>>();
		String query = url.substring(iq + 1);
		String[] params = query.split("&");
		for (String kv : params) {
			int ieq = kv.indexOf("=");
			String k, v;
			if (ieq >= 0) {
				k = kv.substring(0, ieq);
				try {
					v = URLDecoder.decode(kv.substring(ieq + 1), "UTF-8");
				} catch (UnsupportedEncodingException ex) {
					v = kv.substring(ieq + 1);
				}
			} else {
				k = kv;
				v = "";
			}
			List<String> va = map.get(k);
			if (va == null) {
				map.put(k, va = new ArrayList<String>());
			}
			va.add(v);
		}
		Map<String, String[]> parameterMap = new HashMap<String, String[]>();
		for (Entry<String, List<String>> e : map.entrySet()) {
			List<String> va = e.getValue();
			parameterMap.put(e.getKey(), va.toArray(new String[va.size()]));
		}
		return parameterMap;
	}

	protected HttpServletRequest getRequestMock(String requestURI, String query) {
		HttpServletRequest mock = EasyMock.createNiceMock(HttpServletRequest.class);
		// DatelessReplayRequestParser checks out Accept-Datetime HTTP header. If we
		// want to test it, we need to setup return value for getHeader() method.
		// RequestMapper accesses HttpServletequest.getAttribute(RequestMapper.REQUEST_CONTEXT_PREFIX)
		// which can be null. so we leave it null.
		// prefix must end with "/" (see RequestMapper#handleRequest(HttpServletRequest, HttpServletResponse)
		EasyMock.expect(mock.getAttribute(EasyMock.eq("webapp-request-context-path-prefix"))).andStubReturn("/web/");
		EasyMock.expect(mock.getRequestURI()).andStubReturn(requestURI);
		EasyMock.expect(mock.getQueryString()).andStubReturn(query);
		EasyMock.expect(mock.getHeader(MementoConstants.ACCEPT_DATETIME)).andStubReturn(acceptDatetimeHeader);
		EasyMock.expect(mock.getHeader("Accept-Timestamp")).andStubReturn(acceptTimestampHeader);
		// used by OpenSearchRequestParser, TimeMapRequestParser
		final Map<String, String[]> parameterMap = parseParameters(requestURI);
		EasyMock.expect(mock.getParameterMap()).andStubReturn(parameterMap);
		EasyMock.expect(mock.getParameter(EasyMock.<String>notNull()))
		.andStubAnswer(new IAnswer<String>() {
			@Override
			public String answer() throws Throwable {
				String name = (String)EasyMock.getCurrentArguments()[0];
				String[] va = parameterMap.get(name);
				return va != null ? va[0] : null;
			}
		});
		return mock;
	}

	protected WaybackRequest parse(String url) throws BadQueryException, BetterRequestException {
		// note: url is set to HttpServletRequest's requestURI property. it should NOT contain
		// query part. RequestParser will receive value translated by RequestMapper.getRequestContextPath()
		// (i.e. "/web/" part removed.)
		HttpServletRequest req = getRequestMock(url, null);
		EasyMock.replay(req);
		return cut.parse(req, accessPoint);
	}

	public void testReplayRequest() throws Exception {
		WaybackRequest wbr = parse("/web/20100101000000/http://www.yahoo.com/");
		assertNotNull(wbr);
		assertTrue(wbr.isReplayRequest());
		assertEquals("20100101000000", wbr.getReplayTimestamp());
		assertEquals("http://www.yahoo.com/", wbr.getRequestUrl());
	}

	/**
	 * timestamp without "*", path ending with "*".
	 * <p>this is interpreted as replay request for the URL including trailing "*".
	 * That sounds inconsistent with other cases. (BTW, resultant time-redirect URL
	 * has no trailing "*" - there's a special handling happening somewhere.)
	 * Should this be interpreted as URL-query for the specific date?</p>
	 * @throws Exception
	 */
	public void testDatePathPrefix() throws Exception {
		WaybackRequest wbr = parse("/web/20100101000000/http://www.yahoo.com/*");
		assertNotNull(wbr);
		assertTrue(wbr.isReplayRequest());
		assertEquals("20100101000000", wbr.getReplayTimestamp());
		// ReplayRequestParser does not strip trailing "*" off.
		assertEquals("http://www.yahoo.com/*", wbr.getRequestUrl());
	}

	/**
	 * test of {@link PathDatePreofixQueryRequestParser}.
	 */
	public void testDatePrefix() throws Exception {
		// less-than-14-digit timestamp with "*": narrowed time range, highlight
		// the latest within the range.
		WaybackRequest wbr1 = parse("/web/20100101*/http://www.yahoo.com/?p=2");
		assertNotNull(wbr1);
		assertTrue(wbr1.isCaptureQueryRequest());
		assertEquals("20100101000000", wbr1.getStartTimestamp());
		assertEquals("20100101235959", wbr1.getEndTimestamp());
		assertEquals("20100101235959", wbr1.getReplayTimestamp());
		assertEquals("http://www.yahoo.com/?p=2", wbr1.getRequestUrl());

		// just "*": entire time range, replay the latest.
		WaybackRequest wbr2 = parse("/web/*/http://www.yahoo.com/");
		assertNotNull(wbr2);
		assertTrue(wbr2.isCaptureQueryRequest());
		assertEquals(EXPECTED_START_TIMESTAMP, wbr2.getStartTimestamp());
		assertEquals(EXPECTED_END_TIMESTAMP, wbr2.getEndTimestamp());
		assertEquals(null, wbr2.getReplayTimestamp());

		// full 14-digit timestamp with "*": entire time range, highlight the
		// closest to the specified date.
		WaybackRequest wbr3 = parse("/web/20100101000000*/http://www.yahoo.com/");
		assertNotNull(wbr3);
		assertTrue(wbr3.isCaptureQueryRequest());
		assertEquals(EXPECTED_START_TIMESTAMP, wbr3.getStartTimestamp());
		assertEquals(EXPECTED_END_TIMESTAMP, wbr3.getEndTimestamp());
		assertEquals("20100101000000", wbr3.getReplayTimestamp());
	}

	/**
	 * Another test of {@link PathDatePrefixQueryRequestParser}:
	 * URL is recognized even when "{@code *}" is %-encoded.
	 * <p>this is a desired behavior to be implemented (assertion is disabled).
	 * see issue <a href="https://webarchive.jira.com/browse/WWM-110">WWM-110</a></p>
	 * @throws Exception
	 */
	public void testDatePrefixEncoded() throws Exception {
		WaybackRequest wbr1 = parse("/web/20100101%2A/http://www.yahoo.com/?p=%2A");
		assertNotNull(wbr1);
		assertTrue(wbr1.isCaptureQueryRequest());
		assertEquals("20100101000000", wbr1.getStartTimestamp());
		assertEquals("20100101235959", wbr1.getEndTimestamp());
		assertEquals("20100101235959", wbr1.getReplayTimestamp());
		assertEquals("http://www.yahoo.com/?p=%2A", wbr1.getRequestUrl());

		WaybackRequest wbr2 = parse("/web/%2A/http://www.yahoo.com/");
		assertNotNull(wbr2);
		assertTrue(wbr2.isCaptureQueryRequest());
		assertEquals(EXPECTED_START_TIMESTAMP, wbr2.getStartTimestamp());
		assertEquals(EXPECTED_END_TIMESTAMP, wbr2.getEndTimestamp());
		assertEquals(null, wbr2.getReplayTimestamp());
	}

	/**
	 * test for {@link PathDateRangeQueryRequestParser}.
	 * <p>date range and specific path.
	 * date range becomes start and end timestamps. but it doesn't
	 * set replayTimestamp (it should, for consistency?)
	 */
	public void testPathDateRange() throws Exception {
		// range with full-length timestamps
		WaybackRequest wbr1 = parse("/web/20100101000000-20100630235959*/http://www.yahoo.com/");
		assertNotNull(wbr1);
		assertTrue(wbr1.isCaptureQueryRequest());
		assertEquals("20100101000000", wbr1.getStartTimestamp());
		assertEquals("20100630235959", wbr1.getEndTimestamp());
//		assertEquals("20100630235959", wbr1.getReplayTimestamp());
		assertEquals(null, wbr1.getReplayTimestamp());
		assertEquals("http://www.yahoo.com/", wbr1.getRequestUrl());

		WaybackRequest wbr2 = parse("/web/2010-2014*/http://www.yahoo.com/?p=2");
		assertNotNull(wbr2);
		assertTrue(wbr1.isCaptureQueryRequest());
		assertEquals("20100101000000", wbr2.getStartTimestamp());
		assertEquals("20141231235959", wbr2.getEndTimestamp());
//		assertEquals("20141231235959", wbr2.getReplayTimestamp());
		assertEquals(null, wbr2.getReplayTimestamp());
		assertEquals("http://www.yahoo.com/?p=2", wbr2.getRequestUrl());

		// Date range without "*" results in 404. We could make it work.
		WaybackRequest wbr3 = parse("/web/2010-2014/http://www.yahoo.com/");
		assertNull(wbr3);
	}

	/**
	 * test for {@link PathDateRangeQueryRequestParser}, %-encoded version.
	 * @throws Exception
	 */
	public void testPathDateRangeEncoded() throws Exception {
		WaybackRequest wbr1 = parse("/web/20100101000000-20100630235959%2A/http://www.yahoo.com/");
		assertNotNull(wbr1);
		assertTrue(wbr1.isCaptureQueryRequest());
		assertEquals("20100101000000", wbr1.getStartTimestamp());
		assertEquals("20100630235959", wbr1.getEndTimestamp());
//		assertEquals("20100630235959", wbr1.getReplayTimestamp());
		assertEquals(null, wbr1.getReplayTimestamp());
		assertEquals("http://www.yahoo.com/", wbr1.getRequestUrl());
	}

	/**
	 * test of {@link PathPrefixDatePrefixQueryRequestParser}.
	 * <p>this is a URL query with (optional) single timestamp date range.
	 * timestamp, if non-empty, becomes start and end.
	 * </p>
	 */
	public void testPathPrefixDatePrefix() throws Exception {
		WaybackRequest wbr1 = parse("/web/2010*/http://www.yahoo.com/*");
		assertNotNull(wbr1);
		assertTrue(wbr1.isUrlQueryRequest());
		assertEquals("20100101000000", wbr1.getStartTimestamp());
		assertEquals("20101231235959", wbr1.getEndTimestamp());
		// does not set replayTimestamp, but it is not a required behavior.
		assertEquals("http://www.yahoo.com/", wbr1.getRequestUrl());

		WaybackRequest wbr2 = parse("/web/*/http://www.yahoo.com/*");
		assertNotNull(wbr2);
		assertTrue(wbr2.isUrlQueryRequest());
		assertEquals(EXPECTED_START_TIMESTAMP, wbr2.getStartTimestamp());
		assertEquals(null, wbr2.getEndTimestamp());
		// does not set replayTimestamp, but it is not a required behavior.
		assertEquals("http://www.yahoo.com/", wbr2.getRequestUrl());

		// timestamp is up to 13 digits. 14-digit timestamp results in null (-> 404).
		// TODO: there'd be a nicer way.
		WaybackRequest wbr3 = parse("/web/20130101000000*/http://www.yahoo.com/*");
		assertNull(wbr3);
	}
	/**
	 * test of {@link PathPrefixDatePrefixQueryRequestParser}.
	 * <p>%-encoded timestamp.</p>
	 * @throws Exception
	 */
	public void testPathPrefixDatePrefixEncoded() throws Exception {
		{
			WaybackRequest wbr = parse("/web/2010%2A/http://www.yahoo.com/*");
			assertNotNull(wbr);
			assertTrue(wbr.isUrlQueryRequest());
			assertEquals("20100101000000", wbr.getStartTimestamp());
			assertEquals("20101231235959", wbr.getEndTimestamp());
			assertEquals("http://www.yahoo.com/", wbr.getRequestUrl());
		}
		// negative case - %2A doesn't make it path-prefix.
		{
			WaybackRequest wbr = parse("/web/2010%2A/http://www.yahoo.com/%2A");
			assertNotNull(wbr);
			assertTrue(wbr.isCaptureQueryRequest());
			assertEquals("20100101000000", wbr.getStartTimestamp());
			assertEquals("20101231235959", wbr.getEndTimestamp());
			assertEquals("http://www.yahoo.com/%2A", wbr.getRequestUrl());
		}
	}

	/**
	 * test of {@link PathPrefixDateRangeQueryRequestParser}.
	 * <p>explicit timestamp range and trailing "*" in URL.
	 * this is a URL-query request, timestamp range becomes
	 * start and end timestamp.</p>
	 * <p>timerange without "*" is not recognized. it could be.</p>
	 */
	public void testPathPrefixDateRange() throws Exception {
		{
			WaybackRequest wbr1 = parse("/web/20100101-20100531*/http://www.yahoo.com/*");
			assertNotNull(wbr1);
			assertTrue(wbr1.isUrlQueryRequest());
			assertEquals("20100101000000", wbr1.getStartTimestamp());
			assertEquals("20100531235959", wbr1.getEndTimestamp());
			assertEquals("http://www.yahoo.com/", wbr1.getRequestUrl());
		}

		// TODO: date range without "*"
	}

	/**
	 * test of {@link PathPrefixDateRangeQueryRequestParser},
	 * %-encoded version.
	 * @throws Exception
	 */
	public void testPathPrefixdateRangeEncoded() throws Exception {
		{
			WaybackRequest wbr = parse("/web/20100101%2D20100531%2A/http://www.yahoo.com/*");
			assertNotNull(wbr);
			assertTrue(wbr.isUrlQueryRequest());
			assertEquals("20100101000000", wbr.getStartTimestamp());
			assertEquals("20100531235959", wbr.getEndTimestamp());
			assertEquals("http://www.yahoo.com/", wbr.getRequestUrl());
		}
		{
			// URL part shall not be URL-decoded
			WaybackRequest wbr = parse("/web/20100101%2D20100531%2A/http%3a//www.yahoo.com/%2A");
			assertNotNull(wbr);
			assertTrue(wbr.isCaptureQueryRequest());
			assertEquals("20100101000000", wbr.getStartTimestamp());
			assertEquals("20100531235959", wbr.getEndTimestamp());
			// WaybackRequet#setRequestUrl() prepends "http://" if urlStr does
			// not begin with http://
			assertEquals("http://http%3a//www.yahoo.com/%2A", wbr.getRequestUrl());
		}
	}

	protected void checkPathDateless(WaybackRequest wbr, String requestUrl) {
		assertNotNull(wbr);
		assertTrue(wbr.isReplayRequest());
		assertTrue(wbr.isBestLatestReplayRequest());
		assertEquals(EXPECTED_START_TIMESTAMP, wbr.getStartTimestamp());
		assertEquals(EXPECTED_END_TIMESTAMP, wbr.getEndTimestamp());
		assertEquals(requestUrl, wbr.getRequestUrl());
		assertNotNull(wbr.getReplayDate());
		assertNotNull(wbr.getAnchorDate());
	}

	/**
	 * test of {@link DatelessReplayRequestParser}.
	 * <p>It also reads Replay/AchorDate from Accept-Date/Accept-Timestamp
	 * HTTP headers (Accept-Date is part of Memento protocol). This test is
	 * included here, even though it has some Memento-related code, because
	 * core part of processing is for Archival-URL.</p>
	 * <p>One notable difference from with-timestamp request is that
	 * {@link WaybackRequest#isBestLatestReplayRequest} is set to true, which used
	 * in several places for unclear purpose. This flag is not set in other
	 * RequestParser's, even if request is asking for the best latest capture.
	 * Probably flag's semantics is slightly different from what its name suggests.</p>
	 * <p>As this pattern of requests come from URL that escaped Archival-URL
	 * rewriting process, it runs extra check on it. Test includes a few negative cases
	 * for non-URL paths.</p>
	 * @throws Exception
	 */
	public void testPathDateless() throws Exception {
		{
			WaybackRequest wbr = parse("/web/http://www.yahoo.com/");
			checkPathDateless(wbr, "http://www.yahoo.com/");
		}
		{
			WaybackRequest wbr = parse("/web/https://www.yahoo.com/");
			checkPathDateless(wbr, "https://www.yahoo.com/");
		}
		{
			WaybackRequest wbr = parse("/web/http://www.yahoo.com:8080/");
			checkPathDateless(wbr, "http://www.yahoo.com:8080/");
		}
		// some client canonicalizes "//" in path into "/".
		// (why this isn't done for other patterns of requests?)
		{
			WaybackRequest wbr = parse("/web/http:/www.yahoo.com/");
			checkPathDateless(wbr, "http://www.yahoo.com/");
		}
		// doesn't repair "https:/"
		{
			WaybackRequest wbr = parse("/web/https:/www.yahoo.com/");
			//checkPathDateless(wbr, "https://www.yahoo.com/");
			assertNull(wbr);
		}
		// doesn't repair "ftp:/" either.
		{
			WaybackRequest wbr = parse("/web/ftp:/www.yahoo.com/afile");
			assertNull(wbr);
		}
		// scheme-relative - results in NullPointerException FIXME
		{
			try {
				WaybackRequest wbr = parse("/web///www.yahoo.com/");
				assertNull(wbr);
			} catch (NullPointerException ex) {
				// current behavior - FIXME.
			}
		}
		// regular case.
		{
			WaybackRequest wbr = parse("/web/www.yahoo.com/");
			checkPathDateless(wbr, "http://www.yahoo.com/");
		}
		// scheme-less URL with user info is rejected
		// TODO: why is this rejected?
		{
			try {
				@SuppressWarnings("unused")
				WaybackRequest wbr = parse("/web/user@www.yahoo.com/");
				fail("BadQueryException was not thrown");
			} catch (BadQueryException ex) {
				// expected;
			}
		}
		// but it's accepted with scheme.
		// TODO: should this be rejected as well?
		{
			WaybackRequest wbr = parse("/web/http://user@www.yahoo.com/");
			checkPathDateless(wbr, "http://user@www.yahoo.com/");
		}
		// just make sure path and query parts in requestUrl are preserved.
		{
			WaybackRequest wbr = parse("/web/www.yahoo.com/apis?v=2");
			checkPathDateless(wbr, "http://www.yahoo.com/apis?v=2");
		}
		// doesn't look like an URL.
		{
			WaybackRequest wbr = parse("/web/images/foo.gif");
			assertNull(wbr);
		}
		{
			WaybackRequest wbr = parse("/web/handler.php?url=http://www.yahoo.com/");
			assertNull(wbr);
		}
		// TODO: shouldn't this be parsed as dateless URL-Query?
		{
			WaybackRequest wbr = parse("/web/http://www.yahoo.com/*");
			checkPathDateless(wbr, "http://www.yahoo.com/*");
		}
		// ditto
		{
			WaybackRequest wbr = parse("/web/www.yahoo.com/*");
			checkPathDateless(wbr, "http://www.yahoo.com/*");
		}
	}

	public void testPathDatelessWithDateHeader() throws Exception {
		final String dateHeader = "Thu, 24 Apr 2014 21:15:51 UTC+00:00";
		final Date date = (new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.ENGLISH)).parse(dateHeader);
		{
			acceptDatetimeHeader = dateHeader;
			WaybackRequest wbr = parse("/web/http://www.yahoo.com/");
			assertNotNull(wbr);
			assertTrue(wbr.isReplayRequest());
			assertTrue(wbr.isMementoTimegate());
			assertFalse(wbr.isBestLatestReplayRequest());
			assertEquals(EXPECTED_START_TIMESTAMP, wbr.getStartTimestamp());
			assertEquals(EXPECTED_END_TIMESTAMP, wbr.getEndTimestamp());
			assertEquals(date, wbr.getReplayDate());
			assertEquals(date, wbr.getAnchorDate());
		}
		// invalid Accept-Datetime header
		{
			acceptDatetimeHeader = "invalid date";
			try {
				@SuppressWarnings("unused")
				WaybackRequest wbr = parse("/web/http://www.yahoo.com/");
				fail("did not throw exception");
			} catch (TimeGateBadQueryException ex) {
				// expected
			}
		}
		// alternative Accept-Timestamp header. as long as it is valid,
		// invalid value in Accept-Datetime header doesn't cause exception.
		{
			acceptDatetimeHeader = "invalid date";
			acceptTimestampHeader = "20140424211551";
			WaybackRequest wbr = parse("/web/http://www.yahoo.com/");
			assertNotNull(wbr);
			assertTrue(wbr.isReplayRequest());
			assertFalse(wbr.isBestLatestReplayRequest());
			assertEquals(EXPECTED_START_TIMESTAMP, wbr.getStartTimestamp());
			assertEquals(EXPECTED_END_TIMESTAMP, wbr.getEndTimestamp());
			assertEquals(date, wbr.getReplayDate());
			assertEquals(date, wbr.getAnchorDate());
		}
		// invalid value in Accept-Timestamp header is silently ignored,
		// unless Accept-Datetime also has an invalid value.
		{
			acceptDatetimeHeader = null;
			acceptTimestampHeader = "*INVALID*";
			WaybackRequest wbr = parse("/web/http://www.yahoo.com/");
			assertNotNull(wbr);
			assertTrue(wbr.isReplayRequest());
			assertTrue(wbr.isBestLatestReplayRequest());
			assertEquals(EXPECTED_START_TIMESTAMP, wbr.getStartTimestamp());
			assertEquals(EXPECTED_END_TIMESTAMP, wbr.getEndTimestamp());
		}
	}

	/**
	 * some pathological cases.
	 * @throws Exception
	 */
	public void testPathological() throws Exception {
		{
			WaybackRequest wbr = parse("/web/20100101*30/http://www.yahoo.com/?p=*");
			assertNull(wbr);
		}
		{
			WaybackRequest wbr = parse("/web/*20100101*/http://www.yahoo.com/");
			assertNull(wbr);
		}
		{
			WaybackRequest wbr = parse("/web/20100101*im_/http://www.yahoo.com/a.png");
			assertNull(wbr);
		}
		// TODO: should we accept this?
		{
			WaybackRequest wbr = parse("/web//20100101*/http://www.yahoo.com/");
			assertNull(wbr);
		}
	}

	/**
	 * tests for {@link TimeMapRequestParser}
	 * @throws Exception
	 */
	public void testTimeMapRequest() throws Exception {
		{
			WaybackRequest wbr = parse("/web/timemap/link/http://www.yahoo.com/");
			assertNotNull(wbr);
			assertTrue(wbr.isMementoTimemapRequest());
			assertTrue(wbr.isCaptureQueryRequest());
			assertEquals("format", "link", wbr.getMementoTimemapFormat());
			assertEquals("requestUrl", "http://www.yahoo.com/", wbr.getRequestUrl());
		}
		{
			WaybackRequest wbr = parse("/web/timemap/link/www.yahoo.com");
			assertNotNull(wbr);
			assertTrue(wbr.isMementoTimemapRequest());
			assertTrue(wbr.isCaptureQueryRequest());
			assertEquals("format", "link", wbr.getMementoTimemapFormat());
			// WaybackRequest.setRequestUrl() adds missing "http://". Similarly
			// http:/www.yahoo.com will be fixed to "http://www.yahoo.com".
			assertEquals("requestUrl", "http://www.yahoo.com", wbr.getRequestUrl());
		}
		{
			String url = "http://www.yahoo.com";
			String urlp = URLEncoder.encode(url, "UTF-8");
			WaybackRequest wbr = parse("/web/timemap/link?url=" + urlp);
			assertNotNull(wbr);
			assertTrue(wbr.isMementoTimemapRequest());
			assertTrue(wbr.isCaptureQueryRequest());
			// FIXME: this is current behavior. shouldn't this be "link"?
			assertEquals("format", "link?url=" + urlp, wbr.getMementoTimemapFormat());
			assertEquals("requestUrl", url, wbr.getRequestUrl());
		}
		{
			String url = "http://www.yahoo.com/";
			String urlp = URLEncoder.encode(url, "UTF-8");
			WaybackRequest wbr = parse("/web/timemap/?url=" + urlp);
			assertNotNull(wbr);
			assertTrue(wbr.isMementoTimemapRequest());
			assertTrue(wbr.isCaptureQueryRequest());
			// FIXME: again, this is current behavior. value should be "", which shall be
			// interpreted as default value.
			assertEquals("format", "?url=" + urlp, wbr.getMementoTimemapFormat());
			assertEquals("requestUrl", url, wbr.getRequestUrl());
		}
		{
			WaybackRequest wbr = parse("/web/timemap?url=" + URLEncoder.encode("http://www.yahoo.com/", "UTF-8"));
			assertNotNull(wbr);
			// FIXME: This is current behavior. As format is null, isMementoTimemapRequest() returns false,
			// renders calendar view. This looks inconsistent with /web/timemap/?url=... pattern above.
			assertFalse(wbr.isMementoTimemapRequest());
			assertTrue(wbr.isCaptureQueryRequest());
			assertEquals("format", null, wbr.getMementoTimemapFormat());
			assertEquals("requestUrl", "http://www.yahoo.com/", wbr.getRequestUrl());
		}
		{
			String url = "http://www.yahoo.com";
			String urlp = URLEncoder.encode(url, "UTF-8");
			WaybackRequest wbr = parse("/web/timemap/link/?url=" + urlp);
			assertNotNull(wbr);
			assertTrue(wbr.isMementoTimemapRequest());
			assertTrue(wbr.isCaptureQueryRequest());
			assertEquals("format", "link", wbr.getMementoTimemapFormat());
			// FIXME: this is current behavior. requestUrl gets bad value, url
			// parameter gets ignored. most likely results in empty results.
			// Note WaybackRequest.setRequestURI() prepends "http://".
			assertEquals("requestUrl", "http://?url=" + urlp, wbr.getRequestUrl());
			//assertEquals("requestUrl", url, wbr.getRequestUrl());
		}
		{
			WaybackRequest wbr = parse("/web/timemap?url=" + URLEncoder.encode("http://www.yahoo.com/", "UTF-8") +
					"&output=link");
			assertNotNull(wbr);
			assertTrue(wbr.isMementoTimemapRequest());
			assertTrue(wbr.isCaptureQueryRequest());
			assertEquals("format", "link", wbr.getMementoTimemapFormat());
			assertEquals("requestUrl", "http://www.yahoo.com/", wbr.getRequestUrl());
		}
		{
			String url = "http://www.yahoo.com";
			String urlp = URLEncoder.encode(url, "UTF-8");
			WaybackRequest wbr = parse("/web/timemap/?url=" + urlp + "&output=link");
			assertNotNull(wbr);
			assertTrue(wbr.isMementoTimemapRequest());
			assertTrue(wbr.isCaptureQueryRequest());
			// FIXME: this is current behavior. output parameter gets ignored, format gets
			// unexpected value. url parameter works.
			assertEquals("format", "?url=" + urlp + "&output=link", wbr.getMementoTimemapFormat());
			//assertEquals("format", "link", wbr.getMementoTimemapFormat());
			assertEquals("requestUrl", url, wbr.getRequestUrl());
		}
		{
			WaybackRequest wbr = parse("/web/timemap");
			assertNull(wbr);
		}
	}
}
