/*
 * 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.apache.wicket.util.tester;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;

import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.IPageManagerProvider;
import org.apache.wicket.IPageRendererProvider;
import org.apache.wicket.IRequestCycleProvider;
import org.apache.wicket.IRequestListener;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.Page;
import org.apache.wicket.Session;
import org.apache.wicket.ThreadContext;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.AbstractAjaxTimerBehavior;
import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
import org.apache.wicket.ajax.AjaxEventBehavior;
import org.apache.wicket.ajax.form.AjaxFormSubmitBehavior;
import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.IAjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
import org.apache.wicket.behavior.AbstractAjaxBehavior;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.core.request.handler.BookmarkableListenerRequestHandler;
import org.apache.wicket.core.request.handler.BookmarkablePageRequestHandler;
import org.apache.wicket.core.request.handler.IPageProvider;
import org.apache.wicket.core.request.handler.ListenerRequestHandler;
import org.apache.wicket.core.request.handler.PageAndComponentProvider;
import org.apache.wicket.core.request.handler.PageProvider;
import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
import org.apache.wicket.feedback.ExactLevelFeedbackMessageFilter;
import org.apache.wicket.feedback.FeedbackCollector;
import org.apache.wicket.feedback.FeedbackMessage;
import org.apache.wicket.feedback.IFeedbackMessageFilter;
import org.apache.wicket.markup.ContainerInfo;
import org.apache.wicket.markup.IMarkupFragment;
import org.apache.wicket.markup.Markup;
import org.apache.wicket.markup.MarkupParser;
import org.apache.wicket.markup.MarkupResourceStream;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.SubmitLink;
import org.apache.wicket.markup.html.internal.Enclosure;
import org.apache.wicket.markup.html.link.AbstractLink;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.link.ResourceLink;
import org.apache.wicket.markup.parser.XmlPullParser;
import org.apache.wicket.markup.parser.XmlTag;
import org.apache.wicket.mock.MockApplication;
import org.apache.wicket.mock.MockPageManager;
import org.apache.wicket.mock.MockRequestParameters;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.page.IPageManager;
import org.apache.wicket.protocol.http.AjaxEnclosureListener;
import org.apache.wicket.protocol.http.IMetaDataBufferingWebResponse;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.protocol.http.WicketFilter;
import org.apache.wicket.protocol.http.mock.CookieCollection;
import org.apache.wicket.protocol.http.mock.MockHttpServletRequest;
import org.apache.wicket.protocol.http.mock.MockHttpServletResponse;
import org.apache.wicket.protocol.http.mock.MockHttpSession;
import org.apache.wicket.protocol.http.mock.MockServletContext;
import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
import org.apache.wicket.protocol.http.servlet.ServletWebResponse;
import org.apache.wicket.request.IExceptionMapper;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.IRequestMapper;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.cycle.RequestCycleContext;
import org.apache.wicket.request.handler.render.PageRenderer;
import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.request.mapper.IRequestMapperDelegate;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.IResource;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.settings.RequestCycleSettings.RenderStrategy;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Classes;
import org.apache.wicket.util.lang.Generics;
import org.apache.wicket.util.resource.StringResourceStream;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpSession;

/**
 * A helper class to ease unit testing of Wicket applications without the need for a servlet
 * container. See javadoc of <code>WicketTester</code> for example usage. This class can be used as
 * is, but JUnit users should use derived class <code>WicketTester</code>.
 *
 * @see WicketTester
 *
 * @author Ingram Chen
 * @author Juergen Donnerstag
 * @author Frank Bille
 * @author Igor Vaynberg
 *
 * @since 1.2.6
 */
@SuppressWarnings("serial")
public class BaseWicketTester
{
	/** log. */
	private static final Logger log = LoggerFactory.getLogger(BaseWicketTester.class);

	private final ServletContext servletContext;
	private final WebApplication application;
	private final List<MockHttpServletRequest> previousRequests = Generics.newArrayList();
	private final List<MockHttpServletResponse> previousResponses = Generics.newArrayList();
	private MockHttpSession httpSession;
	private boolean followRedirects = true;
	private int redirectCount;
	private MockHttpServletRequest lastRequest;
	private MockHttpServletResponse lastResponse;
	/** current request and response */
	private MockHttpServletRequest request;
	private MockHttpServletResponse response;

	/** current session */
	private Session session;

	/** current request cycle */
	private RequestCycle requestCycle;

	private Page lastRenderedPage;

	private boolean exposeExceptions = true;

	private boolean useRequestUrlAsBase = true;

	private IRequestHandler forcedHandler;

	private IFeedbackMessageFilter originalFeedbackMessageCleanupFilter;

	private ComponentInPage componentInPage;

	// User may provide request header value any time. They get applied (and reset) upon next
	// invocation of processRequest()
	private Map<String, String> preHeader;

	/**
	 * Creates <code>WicketTester</code> and automatically create a <code>WebApplication</code>, but
	 * the tester will have no home page.
	 */
	public BaseWicketTester()
	{
		this(new MockApplication());
	}

	/**
	 * Creates <code>WicketTester</code> and automatically creates a <code>WebApplication</code>.
	 *
	 * @param <C>
	 * @param homePage
	 *            a home page <code>Class</code>
	 */
	public <C extends Page> BaseWicketTester(final Class<C> homePage)
	{
		this(new MockApplication()
		{
			@Override
			public Class<? extends Page> getHomePage()
			{
				return homePage;
			}
		});
	}

	/**
	 * Creates a <code>WicketTester</code>.
	 *
	 * @param application
	 *            a <code>WicketTester</code> <code>WebApplication</code> object
	 */
	public BaseWicketTester(final WebApplication application)
	{
		this(application, (MockServletContext)null);
	}

	/**
	 * Creates a <code>WicketTester</code>.
	 *
	 * @param application
	 *            a <code>WicketTester</code> <code>WebApplication</code> object
	 * @param servletContextBasePath
	 *            the absolute path on disk to the web application's contents (e.g. war root) - may
	 *            be <code>null</code>
	 */
	public BaseWicketTester(final WebApplication application, String servletContextBasePath)
	{
		this(application, new MockServletContext(application, servletContextBasePath));
	}

	/**
	 * Creates a <code>WicketTester</code>.
	 *
	 * @param application
	 *            a <code>WicketTester</code> <code>WebApplication</code> object
	 * @param servletCtx
	 *            the servlet context used as backend
	 */
	public BaseWicketTester(final WebApplication application, final ServletContext servletCtx)
	{
		this(application, servletCtx, true);
	}

	/**
	 * Creates a <code>WicketTester</code>.
	 *
	 * @param application
	 *            a <code>WicketTester</code> <code>WebApplication</code> object
	 * @param init
	 *            force the application to be initialized (default = true)
	 */
	public BaseWicketTester(final WebApplication application, boolean init)
	{
		this(application, null, init);
	}

	/**
	 * Creates a <code>WicketTester</code>.
	 *
	 * @param application
	 *            a <code>WicketTester</code> <code>WebApplication</code> object
	 * @param servletCtx
	 *            the servlet context used as backend
	 * @param init
	 *            force the application to be initialized (default = true)
	 */
	public BaseWicketTester(final WebApplication application, final ServletContext servletCtx,
		boolean init)
	{
		if (!init)
		{
			Args.notNull(application, "application");
		}

		servletContext = servletCtx != null
			? servletCtx
			// If it's provided from the container it's not necessary to mock.
			: !init && application.getServletContext() != null
				? application.getServletContext()
				: new MockServletContext(application, null);

		// If using Arquillian and it's configured in a web.xml it'll be provided. If not, mock it.
		if (application.getWicketFilter() == null)
		{
			final FilterConfig filterConfig = new TestFilterConfig();
			WicketFilter filter = new WicketFilter()
			{
				@Override
				public FilterConfig getFilterConfig()
				{
					return filterConfig;
				}
			};
			application.setWicketFilter(filter);
		}

		httpSession = new MockHttpSession(servletContext);

		ThreadContext.detach();

		this.application = application;

		ThreadContext.setApplication(application);

		if (init)
		{
			if (application.getName() == null)
			{
				application.setName("WicketTesterApplication-" + UUID.randomUUID());
			}

			application.setServletContext(servletContext);
			// initialize the application
			application.initApplication();
		}

		// We don't expect any changes during testing. In addition we avoid creating
		// ModificationWatcher threads tests.
		application.getResourceSettings().setResourcePollFrequency(getResourcePollFrequency());

		// reconfigure application for the test environment
		application.setPageRendererProvider(
			new LastPageRecordingPageRendererProvider(application.getPageRendererProvider()));
		application.setRequestCycleProvider(
			new TestRequestCycleProvider(application.getRequestCycleProvider()));

		// set a feedback message filter that will not remove any messages
		originalFeedbackMessageCleanupFilter = application.getApplicationSettings()
			.getFeedbackMessageCleanupFilter();
		application.getApplicationSettings()
			.setFeedbackMessageCleanupFilter(IFeedbackMessageFilter.NONE);
		IPageManagerProvider pageManagerProvider = newTestPageManagerProvider();
		if (pageManagerProvider != null)
		{
			application.setPageManagerProvider(pageManagerProvider);
		}

		// create a new session when the old one is invalidated
		application.getSessionStore().registerUnboundListener(sessionId -> newSession());

		// prepare session
		setupNextRequestCycle();
	}

	/**
	 * By default Modification Watcher is disabled by default for the tests.
	 *
	 * @return the duration between two checks for changes in the resources
	 */
	protected Duration getResourcePollFrequency()
	{
		return null;
	}

	/**
	 *
	 * @return page manager provider
	 */
	protected IPageManagerProvider newTestPageManagerProvider()
	{
		return new TestPageManagerProvider();
	}

	/**
	 * @return last rendered page
	 */
	public Page getLastRenderedPage()
	{
		return lastRenderedPage;
	}

	private void setupNextRequestCycle()
	{
		request = new MockHttpServletRequest(application, httpSession, servletContext,
			servletRequestLocale());
		request.setURL(request.getContextPath() + request.getServletPath() + "/");

		// assign protocol://host:port to next request unless the last request was ajax
		final boolean assignBaseLocation = lastRequest != null
			&& lastRequest.getHeader("Wicket-Ajax") == null;

		// resume request processing with scheme://host:port from last request
		if (assignBaseLocation)
		{
			request.setScheme(lastRequest.getScheme());
			request.setSecure(lastRequest.isSecure());
			request.setServerName(lastRequest.getServerName());
			request.setServerPort(lastRequest.getServerPort());
		}

		response = new MockHttpServletResponse(request);

		// Preserve response cookies in redirects
		// XXX: is this really needed ? Browsers wont do that, but some
		// Wicket tests assert that a cookie is in the response,
		// even after redirects (see
		// org.apache.wicket.util.cookies.SetCookieAndRedirectTest.statefulPage())
		// They should assert that the cookie is in the next *request*
		if (lastResponse != null)
		{
			List<Cookie> lastResponseCookies = lastResponse.getCookies();
			if (lastResponse.isRedirect())
			{
				CookieCollection responseCookies = new CookieCollection();

				// if the last request is a redirect, all cookies from last response should appear
				// in current response
				// this call will filter duplicates
				responseCookies.addAll(lastResponseCookies);
				for (Cookie cookie : responseCookies.allAsList())
				{
					response.addCookie(cookie);
				}

				// copy all request cookies from last request to the new request because of redirect
				// handling this way, the cookie will be send to the next requested page
				if (lastRequest != null)
				{
					CookieCollection requestCookies = new CookieCollection();
					// this call will filter duplicates
					requestCookies.addAll(lastRequest.getCookies());
					request.addCookies(requestCookies.asList());
				}
			}
			else
			{
				// if the last response is not a redirect
				// - copy last request cookies to collection
				// - copy last response cookies to collection
				// - set only the not expired cookies to the next request
				CookieCollection cookies = new CookieCollection();
				if (lastRequest != null)
				{
					// this call will filter duplicates
					cookies.addAll(lastRequest.getCookies());
				}
				// this call will filter duplicates
				cookies.addAll(lastResponseCookies);
				request.addCookies(cookies.asList());
			}
		}

		ServletWebRequest servletWebRequest = newServletWebRequest();
		requestCycle = application.createRequestCycle(servletWebRequest,
			newServletWebResponse(servletWebRequest));
		ThreadContext.setRequestCycle(requestCycle);

		if (session == null)
		{
			newSession();
		}
	}

	protected Locale servletRequestLocale()
	{
		return Locale.getDefault();
	}

	/**
	 * Cleans up feedback messages. This usually happens on detach, but is disabled in unit testing
	 * so feedback messages can be examined.
	 */
	public void cleanupFeedbackMessages()
	{
		cleanupFeedbackMessages(originalFeedbackMessageCleanupFilter);
	}

	/**
	 * Removes all feedback messages
	 */
	public void clearFeedbackMessages()
	{
		cleanupFeedbackMessages(IFeedbackMessageFilter.ALL);
	}

	/**
	 * Cleans up feedback messages given the specified filter.
	 *
	 * @param filter
	 *            filter used to cleanup messages, accepted messages will be removed
	 */
	protected void cleanupFeedbackMessages(IFeedbackMessageFilter filter)
	{

		IVisitor<Component, Void> clearer = new IVisitor<Component, Void>()
		{
			@Override
			public void component(Component component, IVisit<Void> visit)
			{
				if (component.hasFeedbackMessage())
				{
					component.getFeedbackMessages().clear(filter);
				}
			}
		};
		clearer.component(getLastRenderedPage(), null);
		getLastRenderedPage().visitChildren(clearer);

		getSession().getFeedbackMessages().clear(filter);
	}

	/**
	 * @param servletWebRequest
	 * @return servlet web response
	 */
	protected Response newServletWebResponse(final ServletWebRequest servletWebRequest)
	{
		return new WicketTesterServletWebResponse(servletWebRequest, response);
	}

	/**
	 * @return the configured in the user's application web request
	 */
	private ServletWebRequest newServletWebRequest()
	{
		return (ServletWebRequest)application.newWebRequest(request, request.getFilterPrefix());
	}

	/**
	 *
	 */
	private void newSession()
	{
		ThreadContext.setSession(null);

		// the following will create a new session and put it in the thread context
		session = Session.get();
	}

	/**
	 * @return request object
	 */
	public MockHttpServletRequest getRequest()
	{
		return request;
	}

	/**
	 * @param request
	 */
	public void setRequest(final MockHttpServletRequest request)
	{
		this.request = request;
		applyRequest();
	}

	/**
	 * @param response
	 */
	public void setLastResponse(final MockHttpServletResponse response)
	{
		this.lastResponse = response;
	}

	/**
	 * @return session
	 */
	public Session getSession()
	{
		return session;
	}

	/**
	 * Returns {@link HttpSession} for this environment
	 *
	 * @return session
	 */
	public MockHttpSession getHttpSession()
	{
		return httpSession;
	}

	/**
	 * Returns the {@link Application} for this environment.
	 *
	 * @return application
	 */
	public WebApplication getApplication()
	{
		return application;
	}

	/**
	 * Returns the {@link ServletContext} for this environment
	 *
	 * @return servlet context
	 */
	public ServletContext getServletContext()
	{
		return servletContext;
	}

	/**
	 * Destroys the tester. Restores {@link ThreadContext} to state before instance of
	 * {@link WicketTester} was created.
	 */
	public void destroy()
	{
		try
		{
			ThreadContext.setApplication(application);
			application.internalDestroy();
		}
		finally
		{
			ThreadContext.detach();
		}
	}

	/**
	 * @return true, if process was executed successfully
	 */
	public boolean processRequest()
	{
		return processRequest(null, null);
	}

	/**
	 * Processes the request in mocked Wicket environment.
	 *
	 * @param request
	 *            request to process
	 * @return true, if process was executed successfully
	 */
	public boolean processRequest(final MockHttpServletRequest request)
	{
		return processRequest(request, null);
	}

	/**
	 * Processes the request in mocked Wicket environment.
	 *
	 * @param request
	 *            request to process
	 * @param forcedRequestHandler
	 *            optional parameter to override parsing the request URL and force
	 *            {@link IRequestHandler}
	 * @return true, if process was executed successfully
	 */
	public boolean processRequest(final MockHttpServletRequest request,
		final IRequestHandler forcedRequestHandler)
	{
		return processRequest(request, forcedRequestHandler, false);
	}

	/**
	 * @param forcedRequestHandler
	 * @return true, if process was executed successfully
	 */
	public boolean processRequest(final IRequestHandler forcedRequestHandler)
	{
		return processRequest(null, forcedRequestHandler, false);
	}

	/**
	 * Process the request. This is a fairly central function and is almost always invoked for
	 * executing the request.
	 * <p>
	 * You may subclass processRequest it, to monitor or change any pre-configured value. Request
	 * headers can be configured more easily by calling {@link #addRequestHeader(String, String)}.
	 *
	 * @param forcedRequest
	 *            Can be null.
	 * @param forcedRequestHandler
	 *            Can be null.
	 * @param redirect
	 * @return true, if process was executed successfully
	 */
	protected boolean processRequest(final MockHttpServletRequest forcedRequest,
		final IRequestHandler forcedRequestHandler, final boolean redirect)
	{
		if (forcedRequest != null)
		{
			request = forcedRequest;
		}

		forcedHandler = forcedRequestHandler;

		if (!redirect && getRequest().getHeader("Wicket-Ajax") == null)
		{
			lastRenderedPage = null;
		}

		// Add or replace any system provided header entry with the user provided.
		if ((request != null) && (preHeader != null))
		{
			for (Map.Entry<String, String> entry : preHeader.entrySet())
			{
				if (Strings.isEmpty(entry.getKey()) == false)
				{
					request.setHeader(entry.getKey(), entry.getValue());
				}
			}

			// Reset the user provided headers
			preHeader = null;
		}

		applyRequest();
		requestCycle.scheduleRequestHandlerAfterCurrent(null);

		try
		{
			if (!requestCycle.processRequestAndDetach())
			{
				return false;
			}
		}
		finally
		{
			recordRequestResponse();
			setupNextRequestCycle();
		}

		try
		{
			if (isFollowRedirects() && lastResponse.isRedirect())
			{
				if (redirectCount++ >= 100)
				{
					throw new AssertionError("Possible infinite redirect detected. Bailing out.");
				}

				Url newUrl = Url.parse(lastResponse.getRedirectLocation(),
					Charset.forName(request.getCharacterEncoding()));

				if (isExternalRedirect(lastRequest.getUrl(), newUrl))
				{
					// we can't handle external redirects here
					// just bail out here and let the user's test code
					// check #assertRedirectUrl
					return true;
				}

				if (newUrl.isFull() || newUrl.isContextAbsolute())
				{
					request.setUrl(newUrl);

					final String protocol = newUrl.getProtocol();

					if (protocol != null)
					{
						request.setScheme(protocol);
					}

					request.setSecure("https".equals(protocol));

					if (newUrl.getHost() != null)
					{
						request.setServerName(newUrl.getHost());
					}
					if (newUrl.getPort() != null)
					{
						request.setServerPort(newUrl.getPort());
					}
				}
				else
				{
					// append redirect URL to current URL (what browser would do)
					Url mergedURL = new Url(lastRequest.getUrl().getSegments(),
						newUrl.getQueryParameters());
					mergedURL.concatSegments(newUrl.getSegments());

					request.setUrl(mergedURL);
				}

				processRequest(null, null, true);

				--redirectCount;
			}

			return true;
		}
		finally
		{
			redirectCount = 0;
		}
	}

	/**
	 * Determine whether a given response contains a redirect leading to an external site (which
	 * cannot be replicated in WicketTester). This is done by comparing the previous request's
	 * hostname with the hostname given in the redirect.
	 *
	 * @param requestUrl
	 *            request...
	 * @param newUrl
	 *            ...and the redirect generated in its response
	 * @return true if there is a redirect and it is external, false otherwise
	 */
	private boolean isExternalRedirect(Url requestUrl, Url newUrl)
	{
		String originalHost = requestUrl.getHost();
		String redirectHost = newUrl.getHost();
		Integer originalPort = requestUrl.getPort();
		Integer newPort = newUrl.getPort();

		if (originalHost.equals(redirectHost))
		{
			return false; // identical or both null
		}
		else if (redirectHost == null)
		{
			return false; // no new host
		}
		else if (originalPort.equals(newPort) == false)
		{
			return true;
		}
		else
		{
			return !(redirectHost.equals(originalHost));
		}
	}

	/**
	 * Allows to set Request header value any time. They'll be applied (add/modify) on process
	 * execution {@link #processRequest(MockHttpServletRequest, IRequestHandler, boolean)}. They are
	 * reset immediately after and thus are not re-used for a sequence of requests.
	 * <p>
	 * Deletion (not replace) of pre-configured header value can be achieved by subclassing
	 * {@link #processRequest(MockHttpServletRequest, IRequestHandler, boolean)} and modifying the
	 * request header directly.
	 *
	 * @param key
	 * @param value
	 */
	public final void addRequestHeader(final String key, final String value)
	{
		Args.notEmpty(key, "key");

		if (preHeader == null)
		{
			preHeader = Generics.newHashMap();
		}

		preHeader.put(key, value);
	}

	private void recordRequestResponse()
	{
		lastRequest = request;
		setLastResponse(response);

		previousRequests.add(request);
		previousResponses.add(response);
	}

	/**
	 * Renders the page specified by given {@link IPageProvider}. After render the page instance can
	 * be retrieved using {@link #getLastRenderedPage()} and the rendered document will be available
	 * in {@link #getLastResponse()}.
	 *
	 * Depending on {@link RenderStrategy} invoking this method can mean that a redirect will happen
	 * before the actual render.
	 *
	 * @param pageProvider
	 * @return last rendered page
	 */
	public Page startPage(final IPageProvider pageProvider)
	{
		// should be null for Pages
		componentInPage = null;

		// prepare request
		request.setURL(request.getContextPath() + request.getServletPath() + "/");
		IRequestHandler handler = new RenderPageRequestHandler(pageProvider);

		// process request
		processRequest(request, handler);

		// The page rendered
		return getLastRenderedPage();
	}

	/**
	 * Renders the page.
	 *
	 * @see #startPage(IPageProvider)
	 *
	 * @param page
	 * @return Page
	 */
	@SuppressWarnings("unchecked")
	public <T extends Page> T startPage(final T page)
	{
		return (T)startPage(new PageProvider(page));
	}

	/**
	 * Simulates a request to a mounted {@link IResource}
	 *
	 * @param resource
	 *            the resource to test
	 * @return the used {@link ResourceReference} for the simulation
	 */
	public ResourceReference startResource(final IResource resource)
	{
		return startResourceReference(new ResourceReference("testResourceReference")
		{
			private static final long serialVersionUID = 1L;

			@Override
			public IResource getResource()
			{
				return resource;
			}
		});
	}

	/**
	 * Simulates a request to a mounted {@link ResourceReference}
	 *
	 * @param reference
	 *            the resource reference to test
	 * @return the tested resource reference
	 */
	public ResourceReference startResourceReference(final ResourceReference reference)
	{
		return startResourceReference(reference, null);
	}

	/**
	 * Simulates a request to a mounted {@link ResourceReference}
	 *
	 * @param reference
	 *            the resource reference to test
	 * @param pageParameters
	 *            the parameters passed to the resource reference
	 * @return the tested resource reference
	 */
	public ResourceReference startResourceReference(final ResourceReference reference,
		final PageParameters pageParameters)
	{
		// prepare request
		request.setURL(request.getContextPath() + request.getServletPath() + "/");
		IRequestHandler handler = new ResourceReferenceRequestHandler(reference, pageParameters);

		// execute request
		processRequest(request, handler);

		// the reference processed
		return reference;
	}

	/**
	 * @return last response or <code>null</code> if no response has been produced yet.
	 */
	public MockHttpServletResponse getLastResponse()
	{
		return lastResponse;
	}

	/**
	 * The last response as a string when a page is tested via {@code startPage()} methods.
	 * <p>
	 * In case the processed component was not a {@link Page} then the automatically created page
	 * markup gets removed. If you need the whole returned markup in this case use
	 * {@link #getLastResponse()}{@link MockHttpServletResponse#getDocument() .getDocument()}
	 * </p>
	 *
	 * @return last response as String.
	 */
	public String getLastResponseAsString()
	{
		String response = lastResponse.getDocument();

		// null, if a Page was rendered last
		if (componentInPage == null)
		{
			return response;
		}

		// remove the markup for the auto-generated page. leave just component's markup
		int end = response.lastIndexOf("</body>");
		if (end > -1)
		{
			int start = response.indexOf("<body>") + "<body>".length();
			response = response.substring(start, end);
		}

		return response;
	}

	/**
	 * This method tries to parse the last response to return the encoded base URL and will throw an
	 * exception if there none was encoded.
	 *
	 * @return Wicket-Ajax-BaseURL set on last response by {@link AbstractDefaultAjaxBehavior}
	 * @throws IOException
	 * @throws ParseException
	 */
	public String getWicketAjaxBaseUrlEncodedInLastResponse() throws IOException, ParseException
	{
		XmlPullParser parser = new XmlPullParser();
		parser.parse(getLastResponseAsString());
		XmlTag tag;
		while ((tag = parser.nextTag()) != null)
		{
			if (tag.isOpen() && tag.getName().equals("script")
				&& "wicket-ajax-base-url".equals(tag.getAttribute("id")))
			{
				parser.next();
				return parser.getString().toString().split("\\\"")[1];
			}
		}

		fail("Last response has no AJAX base URL set by AbstractDefaultAjaxBehavior.");
		return null;
	}

	/**
	 * @return list of prior requests
	 */
	public List<MockHttpServletRequest> getPreviousRequests()
	{
		return previousRequests;
	}

	/**
	 * @return list of prior responses
	 */
	public List<MockHttpServletResponse> getPreviousResponses()
	{
		return previousResponses;
	}

	/**
	 * Sets whether responses with redirects will be followed automatically.
	 *
	 * @param followRedirects
	 */
	public void setFollowRedirects(boolean followRedirects)
	{
		this.followRedirects = followRedirects;
	}

	/**
	 * @return <code>true</code> if redirect responses will be followed automatically,
	 *         <code>false</code> otherwise.
	 */
	public boolean isFollowRedirects()
	{
		return followRedirects;
	}

	/**
	 * Encodes the {@link IRequestHandler} to {@link Url}. It should be safe to call this method
	 * outside request thread as log as no registered {@link IRequestMapper} requires a
	 * {@link RequestCycle}.
	 *
	 * @param handler
	 * @return {@link Url} for handler.
	 */
	public Url urlFor(final IRequestHandler handler)
	{
		Url url = application.getRootRequestMapper().mapHandler(handler);
		return transform(url);
	}

	/**
	 * @param link
	 * @return url for Link
	 */
	public String urlFor(Link<?> link)
	{
		Args.notNull(link, "link");

		Url url = Url.parse(link.urlForListener(new PageParameters()).toString());
		return transform(url).toString();
	}

	/**
	 * Simulates processing URL that invokes an {@link IRequestListener} on a component.
	 *
	 * After the listener is invoked the page containing the component will be rendered (with an
	 * optional redirect - depending on {@link RenderStrategy}).
	 *
	 * @param component
	 */
	public void executeListener(final Component component)
	{
		Args.notNull(component, "component");

		// there are two ways to do this. RequestCycle could be forced to call the handler
		// directly but constructing and parsing the URL increases the chance of triggering bugs
		Page page = component.getPage();
		PageAndComponentProvider pageAndComponentProvider = new PageAndComponentProvider(page,
			component);

		IRequestHandler handler = null;
		if (page.isPageStateless() || (page.isBookmarkable() && page.wasCreatedBookmarkable()))
		{
			handler = new BookmarkableListenerRequestHandler(pageAndComponentProvider);
		}
		else
		{
			handler = new ListenerRequestHandler(pageAndComponentProvider);
		}

		Url url = urlFor(handler);
		request.setUrl(url);

		// Process the request
		processRequest(request, null);
	}

	/**
	 * Simulates invoking an {@link IRequestListener} on a component. As opposed to the
	 * {@link #executeListener(Component)} method, current request/response objects will be used
	 *
	 * After the listener is invoked the page containing the component will be rendered (with an
	 * optional redirect - depending on {@link RenderStrategy}).
	 *
	 * @param component
	 */
	public void invokeListener(final Component component)
	{
		Args.notNull(component, "component");

		// there are two ways to do this. RequestCycle could be forced to call the handler
		// directly but constructing and parsing the URL increases the chance of triggering bugs
		IRequestHandler handler = new ListenerRequestHandler(
			new PageAndComponentProvider(component.getPage(), component));

		processRequest(handler);
	}

	/**
	 * Simulates invoking an {@link IRequestListener} on a component. As opposed to the
	 * {@link #executeListener(Component)} method, current request/response objects will be used
	 *
	 * After the listener is invoked the page containing the component will be rendered (with an
	 * optional redirect - depending on {@link RenderStrategy}).
	 *
	 * @param component
	 * @param behavior
	 */
	public void invokeListener(Component component, final Behavior behavior)
	{
		Args.notNull(component, "component");
		Args.notNull(behavior, "behavior");

		// there are two ways to do this. RequestCycle could be forced to call the handler
		// directly but constructing and parsing the URL increases the chance of triggering bugs
		IRequestHandler handler = new ListenerRequestHandler(
			new PageAndComponentProvider(component.getPage(), component),
			component.getBehaviorId(behavior));

		processRequest(handler);
	}

	/**
	 * Builds and processes a request suitable for executing an <code>AbstractAjaxBehavior</code>.
	 *
	 * @param behavior
	 *            an <code>AbstractAjaxBehavior</code> to execute
	 */
	public void executeBehavior(final AbstractAjaxBehavior behavior)
	{
		Args.notNull(behavior, "behavior");

		Url url = Url.parse(behavior.getCallbackUrl().toString(),
			Charset.forName(request.getCharacterEncoding()));
		transform(url);
		request.setUrl(url);
		request.addHeader(WebRequest.HEADER_ORIGIN, createOriginHeader());
		request.addHeader(WebRequest.HEADER_AJAX_BASE_URL, url.toString());
		request.addHeader(WebRequest.HEADER_AJAX, "true");

		if (behavior instanceof AjaxFormSubmitBehavior)
		{
			AjaxFormSubmitBehavior formSubmitBehavior = (AjaxFormSubmitBehavior)behavior;
			Form<?> form = formSubmitBehavior.getForm();
			getRequest().setUseMultiPartContentType(form.isMultiPart());
			serializeFormToRequest(form);

			// mark behavior's component as the form submitter,
			String name = Form.getRootFormRelativeId(
				new PropertyModel<Component>(behavior, "component").getObject());
			if (!request.getPostParameters().getParameterNames().contains(name))
			{
				request.getPostParameters().setParameterValue(name, "marked");
			}
		}

		processRequest();
	}

	/**
	 * Build value to Origin header based on RequestCycle Url
	 *
	 * @return Origin header
	 */
	protected String createOriginHeader()
	{
		Url url = RequestCycle.get().getRequest().getUrl();
		return url.getProtocol() + "://" + url.getHost() + ":" + url.getPort();
	}

	/**
	 *
	 * @param link
	 * @return Url
	 */
	public Url urlFor(final AjaxLink<?> link)
	{
		AbstractAjaxBehavior behavior = WicketTesterHelper.findAjaxEventBehavior(link, "click");
		Url url = Url.parse(behavior.getCallbackUrl().toString(),
			Charset.forName(request.getCharacterEncoding()));
		return transform(url);
	}

	/**
	 *
	 * @param url
	 */
	public void executeAjaxUrl(final Url url)
	{
		Args.notNull(url, "url");

		transform(url);
		request.setUrl(url);
		request.addHeader("Wicket-Ajax-BaseURL", url.toString());
		request.addHeader("Wicket-Ajax", "true");

		processRequest();
	}

	/**
	 * Renders a <code>Page</code> from its default constructor.
	 *
	 * @param <C>
	 * @param pageClass
	 *            a test <code>Page</code> class with default constructor
	 * @return the rendered <code>Page</code>
	 */
	public final <C extends Page> C startPage(final Class<C> pageClass)
	{
		return startPage(pageClass, null);
	}

	/**
	 * Renders a <code>Page</code> from its default constructor.
	 *
	 * @param <C>
	 * @param pageClass
	 *            a test <code>Page</code> class with default constructor
	 * @param parameters
	 *            the parameters to use for the class.
	 * @return the rendered <code>Page</code>
	 */
	@SuppressWarnings("unchecked")
	public final <C extends Page> C startPage(final Class<C> pageClass,
		final PageParameters parameters)
	{
		Args.notNull(pageClass, "pageClass");

		// must be null for Pages
		componentInPage = null;

		// prepare the request
		request.setUrl(application.getRootRequestMapper().mapHandler(
			new BookmarkablePageRequestHandler(new PageProvider(pageClass, parameters))));

		// process the request
		processRequest();

		// The last rendered page
		return (C)getLastRenderedPage();
	}

	/**
	 * Creates a {@link FormTester} for the <code>Form</code> at a given path, and fills all child
	 * {@link org.apache.wicket.markup.html.form.FormComponent}s with blank <code>String</code>s.
	 *
	 * @param path
	 *            path to <code>FormComponent</code>
	 * @return a <code>FormTester</code> instance for testing the <code>Form</code>
	 * @see #newFormTester(String, boolean)
	 */
	public FormTester newFormTester(final String path)
	{
		return newFormTester(path, true);
	}

	/**
	 * Creates a {@link FormTester} for the <code>Form</code> at a given path.
	 *
	 * @param path
	 *            path to <code>FormComponent</code>
	 * @param fillBlankString
	 *            specifies whether to fill all child <code>FormComponent</code>s with blank
	 *            <code>String</code>s
	 * @return a <code>FormTester</code> instance for testing the <code>Form</code>
	 * @see FormTester
	 */
	public FormTester newFormTester(final String path, final boolean fillBlankString)
	{
		return new FormTester(path, (Form<?>)getComponentFromLastRenderedPage(path), this,
			fillBlankString);
	}

	/**
	 * Process a component. A web page will be automatically created with the markup created in
	 * {@link #createPageMarkup(String)}.
	 * <p>
	 * <strong>Note</strong>: the instantiated component will have an auto-generated id. To reach
	 * any of its children use their relative path to the component itself. For example if the
	 * started component has a child a Link component with id "link" then after starting the
	 * component you can click it with: <code>tester.clickLink("link")</code>
	 * </p>
	 *
	 * @param <C>
	 *            the type of the component
	 * @param componentClass
	 *            the class of the component to be tested
	 * @return The component processed
	 * @see #startComponentInPage(org.apache.wicket.Component)
	 */
	public final <C extends Component> C startComponentInPage(final Class<C> componentClass)
	{
		return startComponentInPage(componentClass, null);
	}

	/**
	 * Process a component. A web page will be automatically created with the {@code pageMarkup}
	 * provided. In case pageMarkup is null, the markup will be automatically created with
	 * {@link #createPageMarkup(String)}.
	 * <p>
	 * <strong>Note</strong>: the instantiated component will have an auto-generated id. To reach
	 * any of its children use their relative path to the component itself. For example if the
	 * started component has a child a Link component with id "link" then after starting the
	 * component you can click it with: <code>tester.clickLink("link")</code>
	 * </p>
	 *
	 * @param <C>
	 *            the type of the component
	 *
	 * @param componentClass
	 *            the class of the component to be tested
	 * @param pageMarkup
	 *            the markup for the Page that will be automatically created. May be {@code null}.
	 * @return The component processed
	 */
	public final <C extends Component> C startComponentInPage(final Class<C> componentClass,
		final IMarkupFragment pageMarkup)
	{
		Args.notNull(componentClass, "componentClass");

		// Create the component instance from the class
		C comp = null;
		try
		{
			Constructor<C> c = componentClass.getConstructor(String.class);
			comp = c.newInstance(ComponentInPage.ID);
		}
		catch (Exception e)
		{
			log.error(e.getMessage(), e);
			fail(String.format("Cannot instantiate component with type '%s' because of '%s'",
				componentClass.getName(), e.getMessage()));
		}

		// process the component
		C started = startComponentInPage(comp, pageMarkup);

		componentInPage.isInstantiated = true;

		return started;
	}

	/**
	 * Process a component. A web page will be automatically created with markup created by the
	 * {@link #createPageMarkup(String)}.
	 * <p>
	 * <strong>Note</strong>: the component id is set by the user. To reach any of its children use
	 * this id + their relative path to the component itself. For example if the started component
	 * has id <em>compId</em> and a Link child component component with id "link" then after
	 * starting the component you can click it with: <code>tester.clickLink("compId:link")</code>
	 * </p>
	 *
	 * @param <C>
	 *            the type of the component
	 * @param component
	 *            the component to be tested
	 * @return The component processed
	 * @see #startComponentInPage(Class)
	 */
	public final <C extends Component> C startComponentInPage(final C component)
	{
		return startComponentInPage(component, null);
	}

	/**
	 * Process a component. A web page will be automatically created with the {@code pageMarkup}
	 * provided. In case {@code pageMarkup} is null, the markup will be automatically created with
	 * {@link #createPageMarkup(String)}.
	 * <p>
	 * <strong>Note</strong>: the component id is set by the user. To reach any of its children use
	 * this id + their relative path to the component itself. For example if the started component
	 * has id <em>compId</em> and a Link child component component with id "link" then after
	 * starting the component you can click it with: <code>tester.clickLink("compId:link")</code>
	 * </p>
	 *
	 * @param <C>
	 *            the type of the component
	 * @param component
	 *            the component to be tested
	 * @param pageMarkup
	 *            the markup for the Page that will be automatically created. May be {@code null}.
	 * @return The component processed
	 */
	public final <C extends Component> C startComponentInPage(final C component,
		IMarkupFragment pageMarkup)
	{
		Args.notNull(component, "component");

		// Create a page object and assign the markup
		Page page = createPage();
		if (page == null)
		{
			fail("The automatically created page should not be null.");
		}

		// Automatically create the page markup if not provided
		if (pageMarkup == null)
		{
			String markup = createPageMarkup(component.getId());
			if (markup == null)
			{
				fail("The markup for the automatically created page should not be null.");
			}

			try
			{
				// set a ContainerInfo to be able to use HtmlHeaderContainer so header contribution
				// still work. WICKET-3700
				ContainerInfo containerInfo = new ContainerInfo(page);
				MarkupResourceStream markupResourceStream = new MarkupResourceStream(
					new StringResourceStream(markup), containerInfo, page.getClass());

				MarkupParser markupParser = getApplication().getMarkupSettings().getMarkupFactory()
					.newMarkupParser(markupResourceStream);
				pageMarkup = markupParser.parse();
			}
			catch (Exception e)
			{
				String errorMessage = "Error while parsing the markup for the autogenerated page: "
					+ e.getMessage();
				log.error(errorMessage, e);
				fail(errorMessage);
			}
		}

		if (page instanceof StartComponentInPage)
		{
			((StartComponentInPage)page).setPageMarkup(pageMarkup);
		}
		else
		{
			page.setMarkup(pageMarkup);
		}

		// Add the child component
		page.add(component);

		// Process the page
		startPage(page);

		componentInPage = new ComponentInPage();
		componentInPage.component = component;

		return component;
	}

	/**
	 * Creates the markup that will be used for the automatically created {@link Page} that will be
	 * used to test a component with {@link #startComponentInPage(Class, IMarkupFragment)}
	 *
	 * @param componentId
	 *            the id of the component to be tested
	 * @return the markup for the {@link Page} as {@link String}. Cannot be {@code null}.
	 */
	protected String createPageMarkup(final String componentId)
	{
		return "<html><head></head><body><span wicket:id='" + componentId
			+ "'></span></body></html>";
	}

	/**
	 * Creates a {@link Page} to test a component with
	 * {@link #startComponentInPage(Component, IMarkupFragment)}
	 *
	 * @return a {@link Page} which will contain the component under test as a single child
	 */
	protected Page createPage()
	{
		return new StartComponentInPage();
	}

	public Component getComponentFromLastRenderedPage(String path, boolean wantVisibleInHierarchy)
	{
		return getComponentFromLastRenderedPage(path, wantVisibleInHierarchy, true);
	}

	/**
	 * Gets the component with the given path from last rendered page. This method fails in case the
	 * component couldn't be found.
	 *
	 * @param path
	 *            Path to component
	 * @param wantVisibleInHierarchy
	 *            if true component needs to be visible in hierarchy else {@code null} is returned
	 * @return The component at the path
	 * @see org.apache.wicket.MarkupContainer#get(String)
	 */
	public Component getComponentFromLastRenderedPage(String path, boolean wantVisibleInHierarchy,
		boolean failOnAbsent)
	{
		if (componentInPage != null && componentInPage.isInstantiated)
		{
			String componentIdPageId = componentInPage.component.getId() + ':';
			if (path.startsWith(componentIdPageId) == false)
			{
				path = componentIdPageId + path;
			}
		}

		Component component = getLastRenderedPage().get(path);
		if (component == null)
		{
			if (failOnAbsent)
			{
				fail("path: '" + path + "' does not exist for page: "
					+ Classes.simpleName(getLastRenderedPage().getClass()));
			}
			return null;
		}

		if (!wantVisibleInHierarchy || component.isVisibleInHierarchy())
		{
			return component;
		}

		// Not found or not visible
		return null;
	}

	/**
	 * Gets the component with the given path from last rendered page. This method fails in case the
	 * component couldn't be found, and it will return null if the component was found, but is not
	 * visible.
	 *
	 * @param path
	 *            Path to component
	 * @return The component at the path
	 * @see org.apache.wicket.MarkupContainer#get(String)
	 */
	public Component getComponentFromLastRenderedPage(String path)
	{
		return getComponentFromLastRenderedPage(path, true);
	}

	/**
	 * Returns the first {@link Component} (breadth-first search) matching the given Wicket-ID. If
	 * no such {@link Component} exists it returns {@link Optional#empty()}
	 *
	 * @param wicketId
	 *            the Wicket-ID of the {@link Component} to be found
	 * @return Optional of the component, {@link Optional#empty()} if no matching component can be
	 *         found or wicketId is null or blank.
	 */
	public Optional<Component> getFirstComponentByWicketId(String wicketId)
	{
		if (wicketId == null || wicketId.isBlank())
		{
			return Optional.empty();
		}

		if (getLastRenderedPage() != null && componentInPage != null)
		{
			Component component = getLastRenderedPage().visitChildren((c, visit) -> {
				if (c.getId().equals(wicketId))
				{
					visit.stop(c);
				}
			});

			return Optional.ofNullable(component);
		}

		return Optional.empty();
	}

	/**
	 * Returns a {@link List} of all {@link Component}s matching the given Wicket-ID. Returns an
	 * empty list if no such component can be found or the Wicket-ID is null.
	 *
	 * @param wicketId
	 *            the Wicket-ID of the components to be found
	 * @return A list of all {@link Component}s that have the given Wicket-ID, an empty List if no
	 *         such component can be found. Returns an empty List of wicketId is null or blank.
	 */
	public List<Component> getAllComponentsByWicketId(String wicketId)
	{
		var result = new ArrayList<Component>();

		if (wicketId == null || wicketId.isBlank())
		{
			return result;
		}

		if (getLastRenderedPage() != null && componentInPage != null)
		{
			getLastRenderedPage().visitChildren((c, visit) -> {
				if (c.getId().equals(wicketId))
				{
					result.add(c);
				}
			});
		}

		log.debug("Found {} Components with ID '{}'", result.size(), wicketId);

		return result;
	}

	/**
	 * assert the text of <code>Label</code> component.
	 *
	 * @param path
	 *            path to <code>Label</code> component
	 * @param expectedLabelText
	 *            expected label text
	 * @return a <code>Result</code>
	 */
	public Result hasLabel(String path, String expectedLabelText)
	{
		Label label = (Label)getComponentFromLastRenderedPage(path);
		return isEqual(expectedLabelText, label.getDefaultModelObjectAsString());
	}

	/**
	 * assert component class
	 *
	 * @param <C>
	 *
	 * @param path
	 *            path to component
	 * @param expectedComponentClass
	 *            expected component class
	 * @return a <code>Result</code>
	 */
	public <C extends Component> Result isComponent(String path, Class<C> expectedComponentClass)
	{
		Component component = assertExists(path);

		return isTrue(
			"component '" + Classes.name(component.getClass()) + "' is not of type: "
				+ Classes.name(expectedComponentClass),
			expectedComponentClass.isAssignableFrom(component.getClass()));
	}

	/**
	 * assert component visible.
	 *
	 * @param path
	 *            path to component
	 * @return a <code>Result</code>
	 */
	public Result isVisible(final String path)
	{
		final Result result;

		Component component = getComponentFromLastRenderedPage(path, false);
		if (component == null)
		{
			result = Result.fail("path: '" + path + "' does not exist for page: "
				+ Classes.simpleName(getLastRenderedPage().getClass()));
		}
		else
		{
			result = isTrue("component '" + path + "' is not visible",
				component.isVisibleInHierarchy());
		}

		return result;
	}

	/**
	 * assert component invisible.
	 *
	 * @param path
	 *            path to component
	 * @return a <code>Result</code>
	 */
	public Result isInvisible(final String path)
	{
		final Result result;

		Component component = getComponentFromLastRenderedPage(path, false);
		if (component == null)
		{
			result = Result.fail("path: '" + path + "' does not exist for page: "
				+ Classes.simpleName(getLastRenderedPage().getClass()));
		}
		else
		{
			result = isFalse("component '" + path + "' is visible",
				component.isVisibleInHierarchy());
		}

		return result;
	}

	/**
	 * assert component enabled.
	 *
	 * @param path
	 *            path to component
	 * @return a <code>Result</code>
	 */
	public Result isEnabled(final String path)
	{
		Component component = assertExists(path);

		return isTrue("component '" + path + "' is disabled", component.isEnabledInHierarchy());
	}

	/**
	 * assert component disabled.
	 *
	 * @param path
	 *            path to component
	 * @return a <code>Result</code>
	 */
	public Result isDisabled(final String path)
	{
		Component component = assertExists(path);

		return isFalse("component '" + path + "' is enabled", component.isEnabledInHierarchy());
	}

	public Component assertExists(final String path)
	{
		Component component = getComponentFromLastRenderedPage(path);
		if (component == null)
		{
			fail("path: '" + path + "' does not exist for page: "
				+ Classes.simpleName(getLastRenderedPage().getClass()));
			return null;
		}
		return component;
	}

	public void assertNotExists(final String path)
	{
		Component component = getComponentFromLastRenderedPage(path, true, false);
		if (component != null)
		{
			fail("path: '" + path + "' does exists for page: "
				+ Classes.simpleName(getLastRenderedPage().getClass()));
		}
	}

	private FormComponent<?> assertFormComponent(final String path)
	{
		Component component = assertExists(path);

		if (component instanceof FormComponent<?> == false)
		{
			fail("path: '" + path + "' is not a form component");
			return null;
		}
		return (FormComponent<?>)component;
	}

	/**
	 * assert component required.
	 *
	 * @param path
	 *            path to component
	 * @return a <code>Result</code>
	 */
	public Result isRequired(String path)
	{
		FormComponent<?> formComponent = assertFormComponent(path);

		return isRequired(formComponent);
	}

	/**
	 * assert component required.
	 *
	 * @param component
	 *            a form component
	 * @return a <code>Result</code>
	 */
	public Result isRequired(FormComponent<?> component)
	{
		return isTrue("component '" + component + "' is not required", component.isRequired());
	}

	/**
	 * Asserts that a component is not required.
	 *
	 * @param path
	 *            path to component
	 * @return a <code>Result</code>
	 */
	public Result isNotRequired(String path)
	{
		FormComponent<?> formComponent = assertFormComponent(path);

		return isNotRequired(formComponent);
	}

	/**
	 * Asserts that a component is not required.
	 *
	 * @param component
	 *            a form component
	 * @return a <code>Result</code>
	 */
	public Result isNotRequired(FormComponent<?> component)
	{
		return isFalse("component '" + component + "' is required", component.isRequired());
	}

	/**
	 * assert the content of last rendered page contains(matches) regex pattern.
	 *
	 * @param pattern
	 *            reqex pattern to match
	 * @return a <code>Result</code>
	 */
	public Result ifContains(String pattern)
	{
		return isTrue("pattern '" + pattern + "' not found in:\n" + getLastResponseAsString(),
			getLastResponseAsString().matches("(?s).*" + pattern + ".*"));
	}

	/**
	 * assert the content of last rendered page contains(matches) regex pattern.
	 *
	 * @param pattern
	 *            reqex pattern to match
	 * @return a <code>Result</code>
	 */
	public Result ifContainsNot(String pattern)
	{
		return isFalse("pattern '" + pattern + "' found",
			getLastResponseAsString().matches("(?s).*" + pattern + ".*"));
	}

	/**
	 * Click the {@link Link} in the last rendered Page.
	 * <p>
	 * Simulate that AJAX is enabled.
	 *
	 * @see WicketTester#clickLink(String, boolean)
	 * @param path
	 *            Click the <code>Link</code> in the last rendered Page.
	 */
	public void clickLink(String path)
	{
		clickLink(path, true);
	}

	/**
	 * Click the {@link Link} in the last rendered Page.
	 * <p>
	 * This method also works for {@link AjaxLink}, {@link AjaxFallbackLink} and
	 * {@link AjaxSubmitLink}.
	 * <p>
	 * On AjaxLinks and AjaxFallbackLinks the onClick method is invoked with a valid
	 * AjaxRequestTarget. In that way you can test the flow of your application when using AJAX.
	 * <p>
	 * When clicking an AjaxSubmitLink the form, which the AjaxSubmitLink is attached to is first
	 * submitted, and then the onSubmit method on AjaxSubmitLink is invoked. If you have changed
	 * some values in the form during your test, these will also be submitted. This should not be
	 * used as a replacement for the {@link FormTester} to test your forms. It should be used to
	 * test that the code in your onSubmit method in AjaxSubmitLink actually works.
	 * <p>
	 * This method is also able to simulate that AJAX (javascript) is disabled on the client. This
	 * is done by setting the isAjax parameter to false. If you have an AjaxFallbackLink you can
	 * then check that it doesn't fail when invoked as a normal link.
	 *
	 * @param path
	 *            path to <code>Link</code> component
	 * @param isAjax
	 *            Whether to simulate that AJAX (javascript) is enabled or not. If it's false then
	 *            AjaxLink and AjaxSubmitLink will fail, since it wouldn't work in real life.
	 *            AjaxFallbackLink will be invoked with null as the AjaxRequestTarget parameter.
	 */
	public void clickLink(String path, boolean isAjax)
	{
		Component linkComponent = getComponentFromLastRenderedPage(path);

		checkUsability(linkComponent, true);

		// if the link is an AjaxLink, we process it differently
		// than a normal link
		if (linkComponent instanceof AjaxLink)
		{
			// If it's not ajax we fail
			if (isAjax == false)
			{
				fail("Link " + path + "is an AjaxLink and will "
					+ "not be invoked when AJAX (javascript) is disabled.");
			}

			List<AjaxEventBehavior> behaviors = WicketTesterHelper
				.findAjaxEventBehaviors(linkComponent, "click");
			for (AjaxEventBehavior behavior : behaviors)
			{
				executeBehavior(behavior);
			}
		}
		// if the link is an AjaxSubmitLink, we need to find the form
		// from it using reflection so we know what to submit.
		else if (linkComponent instanceof AjaxSubmitLink)
		{
			// If it's not ajax we fail
			if (isAjax == false)
			{
				fail("Link " + path + " is an AjaxSubmitLink and "
					+ "will not be invoked when AJAX (javascript) is disabled.");
			}

			AjaxSubmitLink link = (AjaxSubmitLink)linkComponent;

			String pageRelativePath = link.getInputName();
			request.getPostParameters().setParameterValue(pageRelativePath, "x");

			submitAjaxFormSubmitBehavior(link,
				(AjaxFormSubmitBehavior)WicketTesterHelper.findAjaxEventBehavior(link, "click"));
		}
		// if the link is an IAjaxLink, use it (do check if AJAX is expected)
		else if (isAjax
			&& (linkComponent instanceof IAjaxLink || linkComponent instanceof AjaxFallbackLink))
		{
			List<AjaxEventBehavior> behaviors = WicketTesterHelper
				.findAjaxEventBehaviors(linkComponent, "click");
			for (AjaxEventBehavior behavior : behaviors)
			{
				executeBehavior(behavior);
			}
		}
		/*
		 * If the link is a submitlink then we pretend to have clicked it
		 */
		else if (linkComponent instanceof SubmitLink)
		{
			SubmitLink submitLink = (SubmitLink)linkComponent;

			String pageRelativePath = submitLink.getInputName();
			request.getPostParameters().setParameterValue(pageRelativePath, "x");

			serializeFormToRequest(submitLink.getForm());
			submitForm(submitLink.getForm().getPageRelativePath());
		}
		else if (linkComponent instanceof ExternalLink)
		{
			ExternalLink externalLink = (ExternalLink)linkComponent;
			String href = externalLink.getDefaultModelObjectAsString();
			try
			{
				getResponse().sendRedirect(href);
				recordRequestResponse();
				setupNextRequestCycle();
			}
			catch (IOException iox)
			{
				throw new WicketRuntimeException("An error occurred while redirecting to: " + href,
					iox);
			}
		}
		// if the link is a normal link (or ResourceLink)
		else if (linkComponent instanceof AbstractLink)
		{
			AbstractLink link = (AbstractLink)linkComponent;

			/*
			 * If the link is a bookmarkable link, then we need to transfer the parameters to the
			 * next request.
			 */
			if (link instanceof BookmarkablePageLink)
			{
				BookmarkablePageLink<?> bookmarkablePageLink = (BookmarkablePageLink<?>)link;
				try
				{
					Method getParametersMethod = BookmarkablePageLink.class
						.getDeclaredMethod("getPageParameters", (Class<?>[])null);
					getParametersMethod.setAccessible(true);

					PageParameters parameters = (PageParameters)getParametersMethod
						.invoke(bookmarkablePageLink, (Object[])null);

					startPage(bookmarkablePageLink.getPageClass(), parameters);
				}
				catch (Exception e)
				{
					throw new WicketRuntimeException("Internal error in WicketTester. "
						+ "Please report this in Wicket's Issue Tracker.", e);
				}
			}
			else if (link instanceof ResourceLink)
			{
				try
				{
					Method getURL = ResourceLink.class.getDeclaredMethod("getURL");
					getURL.setAccessible(true);
					CharSequence url = (CharSequence)getURL.invoke(link);
					executeUrl(url.toString());
				}
				catch (Exception x)
				{
					throw new RuntimeException("An error occurred while clicking on a ResourceLink",
						x);
				}
			}
			else
			{
				executeListener(link);
			}
		}
		// The link requires AJAX
		else if (linkComponent instanceof IAjaxLink && isAjax == false)
		{
			fail("Link " + path + "is an IAjaxLink and will "
				+ "not be invoked when AJAX (javascript) is disabled.");
		}
		else
		{
			fail("Link " + path + " is not an instance of AbstractLink or IAjaxLink");
		}
	}

	/**
	 * Submit the given form in the last rendered {@link Page}
	 * <p>
	 * <strong>Note</strong>: Form request parameters have to be set explicitly.
	 *
	 * @param form
	 *            path to component
	 */
	public void submitForm(Form<?> form)
	{
		submitForm(form.getPageRelativePath());
	}

	/**
	 * Submits the {@link Form} in the last rendered {@link Page}.
	 * <p>
	 * <strong>Note</strong>: Form request parameters have to be set explicitely.
	 *
	 * @param path
	 *            path to component
	 */
	public void submitForm(String path)
	{
		Form<?> form = (Form<?>)getComponentFromLastRenderedPage(path);
		Url url = Url.parse(form.getRootForm().urlForListener(new PageParameters()).toString(),
			Charset.forName(request.getCharacterEncoding()));

		// make url absolute
		transform(url);

		request.setUrl(url);
		processRequest();
	}

	/**
	 * make url suitable for wicket tester use. usually this involves stripping any leading ..
	 * segments to make the url absolute
	 *
	 * @param url
	 * @return Url
	 */
	private Url transform(final Url url)
	{
		while (url.getSegments().size() > 0
			&& (url.getSegments().get(0).equals("..") || url.getSegments().get(0).equals(".")))
		{
			url.getSegments().remove(0);
		}
		return url;
	}

	/**
	 * Asserts the last rendered <code>Page</code> class.
	 *
	 * @param <C>
	 * @param expectedRenderedPageClass
	 *            expected class of last rendered page
	 * @return a <code>Result</code>
	 */
	public <C extends Page> Result isRenderedPage(Class<C> expectedRenderedPageClass)
	{
		Args.notNull(expectedRenderedPageClass, "expectedRenderedPageClass");

		Page page = getLastRenderedPage();
		if (page == null)
		{
			return Result.fail("page was null");
		}
		if (!expectedRenderedPageClass.isAssignableFrom(page.getClass()))
		{
			return Result.fail(String.format("classes not the same, expected '%s', current '%s'",
				expectedRenderedPageClass, page.getClass()));
		}
		return Result.pass();
	}

	/**
	 * Asserts last rendered <code>Page</code> against an expected HTML document.
	 * <p>
	 * Use <code>-Dwicket.replace.expected.results=true</code> to automatically replace the expected
	 * output file.
	 * </p>
	 *
	 * @param pageClass
	 *            used to load the <code>File</code> (relative to <code>clazz</code> package)
	 * @param filename
	 *            expected output <code>File</code> name
	 * @throws Exception
	 */
	public void assertResultPage(final Class<?> pageClass, final String filename) throws Exception
	{
		// Validate the document
		String document = getLastResponseAsString();
		DiffUtil.validatePage(document, pageClass, filename, true);
	}

	/**
	 * Asserts last rendered <code>Page</code> against an expected HTML document as a
	 * <code>String</code>.
	 *
	 * @param expectedDocument
	 *            expected output
	 * @return a <code>Result</code>
	 */
	public Result isResultPage(final String expectedDocument)
	{
		// Validate the document
		String document = getLastResponseAsString();
		return isTrue("expected rendered page equals", document.equals(expectedDocument));
	}

	/**
	 * Asserts no error-level feedback messages.
	 *
	 * @return a <code>Result</code>
	 * @see #hasNoFeedbackMessage(int)
	 */
	public Result hasNoErrorMessage()
	{
		return hasNoFeedbackMessage(FeedbackMessage.ERROR);
	}

	/**
	 * Asserts no info-level feedback messages.
	 *
	 * @return a <code>Result</code>
	 * @see #hasNoFeedbackMessage(int)
	 */
	public Result hasNoInfoMessage()
	{
		return hasNoFeedbackMessage(FeedbackMessage.INFO);
	}

	/**
	 * Asserts there are no feedback messages with the given level.
	 *
	 * @param level
	 *            the level of the feedback message
	 * @return a <code>Result</code>
	 */
	public Result hasNoFeedbackMessage(int level)
	{
		List<Serializable> messages = getMessages(level);
		return isTrue(String.format("expected no %s message, but contains\n%s",
			new FeedbackMessage(null, "", level).getLevelAsString().toLowerCase(Locale.ROOT),
			WicketTesterHelper.asLined(messages)), messages.isEmpty());
	}

	/**
	 * Retrieves <code>FeedbackMessages</code>.
	 *
	 * @param level
	 *            level of feedback message, for example:
	 *            <code>FeedbackMessage.DEBUG or FeedbackMessage.INFO.. etc</code>
	 * @return <code>List</code> of messages (as <code>String</code>s)
	 * @see FeedbackMessage
	 */
	public List<Serializable> getMessages(final int level)
	{
		List<FeedbackMessage> messages = getFeedbackMessages(
			new ExactLevelFeedbackMessageFilter(level));

		List<Serializable> actualMessages = Generics.newArrayList();
		for (FeedbackMessage message : messages)
		{
			actualMessages.add(message.getMessage());
		}
		return actualMessages;
	}

	/**
	 * Retrieves <code>FeedbackMessages</code>.
	 *
	 * @param filter
	 *            A filter that decides which messages to collect
	 * @return <code>List</code> of messages (as <code>String</code>s)
	 * @see FeedbackMessage
	 */
	public List<FeedbackMessage> getFeedbackMessages(final IFeedbackMessageFilter filter)
	{
		return new FeedbackCollector(getLastRenderedPage(), true).collect(filter);
	}

	/**
	 * Dumps the source of last rendered <code>Page</code>.
	 */
	public void dumpPage()
	{
		log.info(getLastResponseAsString());
	}

	/**
	 * Dumps the <code>Component</code> trees.
	 */
	public void debugComponentTrees()
	{
		debugComponentTrees("");
	}

	/**
	 * Dumps the <code>Component</code> trees to log. Show only the <code>Component</code>s whose
	 * paths contain the filter <code>String</code>.
	 *
	 * @param filter
	 *            a filter <code>String</code>
	 */
	public void debugComponentTrees(String filter)
	{
		log.info("debugging ----------------------------------------------");
		for (WicketTesterHelper.ComponentData obj : WicketTesterHelper
			.getComponentData(getLastRenderedPage()))
		{
			if (obj.path.matches(".*" + filter + ".*"))
			{
				var enabled = obj.isEnabled ? "E" : "-";
				var visible = obj.isVisible ? "V" : "-";
				log.info("[{}{}] path\t{} \t{} \t[{}]", enabled, visible, obj.path, obj.type,
					obj.value);
			}
		}
	}

	/**
	 * Tests that a <code>Component</code> has been added to a <code>AjaxRequestTarget</code>, using
	 * {@link org.apache.wicket.ajax.AjaxRequestTarget#add(org.apache.wicket.Component...)}. This
	 * method actually tests that a <code>Component</code> is on the Ajax response sent back to the
	 * client.
	 * <p>
	 * PLEASE NOTE! This method doesn't actually insert the <code>Component</code> in the client DOM
	 * tree, using JavaScript. But it shouldn't be needed because you have to trust that the Wicket
	 * Ajax JavaScript just works.
	 *
	 * @param component
	 *            the <code>Component</code> to test
	 * @return a <code>Result</code>
	 */
	public Result isComponentOnAjaxResponse(final Component component)
	{
		String failMessage = "A component which is null could not have been added to the AJAX response";
		notNull(failMessage, component);

		Result result;

		// test that the component renders the placeholder tag if it's not visible
		String componentInfo = component.toString();
		if (!component.isVisible())
		{
			failMessage = "A component which is invisible and doesn't render a placeholder tag"
				+ " will not be rendered at all and thus won't be accessible for subsequent AJAX interaction. "
				+ componentInfo;
			result = isTrue(failMessage, component.getOutputMarkupPlaceholderTag());
			if (result.wasFailed())
			{
				return result;
			}
		}

		// Get the AJAX response
		String ajaxResponse = getLastResponseAsString();

		// Test that the previous response was actually a AJAX response
		failMessage = "The previous response was not an AJAX response. "
			+ "You need to execute an AJAX event, using #clickLink() or "
			+ "#executeAjaxEvent(), before using this assertion method";
		boolean isAjaxResponse = Pattern
			.compile("^<\\?xml version=\"1.0\" encoding=\".*?\"\\?><ajax-response>")
			.matcher(ajaxResponse).find();
		result = isTrue(failMessage, isAjaxResponse);
		if (result.wasFailed())
		{
			return result;
		}

		// See if the component has a markup id
		String markupId = component.getMarkupId();

		failMessage = "The component doesn't have a markup id, "
			+ "which means that it can't have been added to the AJAX response. " + componentInfo;
		result = isTrue(failMessage, !Strings.isEmpty(markupId));
		if (result.wasFailed())
		{
			return result;
		}

		// Look for that the component is on the response, using the markup id
		boolean isComponentInAjaxResponse = ajaxResponse
			.matches("(?s).*<component id=\"" + markupId + "\"[^>]*?>.*");
		failMessage = "Component wasn't found in the AJAX response. " + componentInfo;
		result = isTrue(failMessage, isComponentInAjaxResponse);

		if (!result.wasFailed())
		{
			return result;
		}

		// Check if the component has been included as part of an enclosure render
		Enclosure enclosure = getLastRenderedPage().visitChildren(Enclosure.class,
			(Enclosure enc, IVisit<Enclosure> visit) -> {
				if (AjaxEnclosureListener.isControllerOfEnclosure(component, enc))
				{
					visit.stop(enc);
				}
			});

		if (enclosure == null)
		{
			return result;
		}

		failMessage = "Component's enclosure was not found in the AJAX response. " + enclosure;
		boolean isEnclosureInAjaxResponse = !isComponentOnAjaxResponse(enclosure).wasFailed();
		return isTrue(failMessage, isEnclosureInAjaxResponse);

	}

	/**
	 * Simulates the firing of an Ajax event.
	 *
	 * @see #executeAjaxEvent(Component, String)
	 *
	 * @since 1.2.3
	 * @param componentPath
	 *            the <code>Component</code> path
	 * @param event
	 *            the event which we simulate being fired. If <code>event</code> is
	 *            <code>null</code>, the test will fail.
	 */
	public void executeAjaxEvent(final String componentPath, final String event)
	{
		Component component = getComponentFromLastRenderedPage(componentPath);
		executeAjaxEvent(component, event);
	}

	/**
	 * Simulates the firing of all ajax timer behaviors on the page
	 *
	 * @param page
	 *            the page which timers will be executed
	 */
	public void executeAllTimerBehaviors(final MarkupContainer page)
	{
		// execute all timer behaviors for the page itself
		internalExecuteAllTimerBehaviors(page);

		// and for all its children
		page.visitChildren(Component.class, new IVisitor<Component, Void>()
		{
			@Override
			public void component(final Component component, final IVisit<Void> visit)
			{
				internalExecuteAllTimerBehaviors(component);
			}
		});
	}

	private void internalExecuteAllTimerBehaviors(final Component component)
	{
		List<AbstractAjaxTimerBehavior> behaviors = component
			.getBehaviors(AbstractAjaxTimerBehavior.class);
		for (AbstractAjaxTimerBehavior timer : behaviors)
		{
			checkUsability(component, true);

			if (!timer.isStopped())
			{
				if (log.isDebugEnabled())
				{
					log.debug("Triggering AjaxSelfUpdatingTimerBehavior: {}",
						component.getClassRelativePath());
				}

				executeBehavior(timer);
			}
		}
	}

	/**
	 * Simulates the firing of an Ajax event. You add an Ajax event to a <code>Component</code> by
	 * using:
	 *
	 * <pre>
	 *     ...
	 *     component.add(new AjaxEventBehavior(&quot;ondblclick&quot;) {
	 *         public void onEvent(AjaxRequestTarget) {}
	 *     });
	 *     ...
	 * </pre>
	 *
	 * You can then test that the code inside <code>onEvent</code> actually does what it's supposed
	 * to, using the <code>WicketTester</code>:
	 *
	 * <pre>
	 *     ...
	 *     tester.executeAjaxEvent(component, &quot;ondblclick&quot;);
	 *     // Test that the code inside onEvent is correct.
	 *     ...
	 * </pre>
	 *
	 * This also works with <code>AjaxFormSubmitBehavior</code>, where it will "submit" the
	 * <code>Form</code> before executing the command.
	 * <p>
	 * PLEASE NOTE! This method doesn't actually insert the <code>Component</code> in the client DOM
	 * tree, using JavaScript.
	 *
	 * @param component
	 *            the <code>Component</code> that has the <code>AjaxEventBehavior</code> we want to
	 *            test. If the <code>Component</code> is <code>null</code>, the test will fail.
	 * @param event
	 *            the event to simulate being fired. If <code>event</code> is <code>null</code>, the
	 *            test will fail.
	 */
	public void executeAjaxEvent(final Component component, final String event)
	{
		Args.notNull(component, "component");
		Args.notNull(event, "event");

		checkUsability(component, true);

		List<AjaxEventBehavior> ajaxEventBehaviors = WicketTesterHelper
			.findAjaxEventBehaviors(component, event);
		for (AjaxEventBehavior ajaxEventBehavior : ajaxEventBehaviors)
		{
			executeBehavior(ajaxEventBehavior);
		}
	}

	/**
	 * Retrieves a <code>TagTester</code> based on a <code>wicket:id</code>. If more
	 * <code>Component</code>s exist with the same <code>wicket:id</code> in the markup, only the
	 * first one is returned.
	 *
	 * @param wicketId
	 *            the <code>wicket:id</code> to search for
	 * @return the <code>TagTester</code> for the tag which has the given <code>wicket:id</code>
	 */
	public TagTester getTagByWicketId(String wicketId)
	{
		return TagTester.createTagByAttribute(getLastResponseAsString(), "wicket:id", wicketId);
	}

	/**
	 * Modified version of BaseWicketTester#getTagByWicketId(String) that returns all matching tags
	 * instead of just the first.
	 *
	 * @param wicketId
	 * @return List of Tags
	 */
	public List<TagTester> getTagsByWicketId(String wicketId)
	{
		return TagTester.createTagsByAttribute(getLastResponseAsString(), "wicket:id", wicketId,
			false);
	}

	/**
	 * Retrieves a <code>TagTester</code> based on an DOM id. If more <code>Component</code>s exist
	 * with the same id in the markup, only the first one is returned.
	 *
	 * @param id
	 *            the DOM id to search for.
	 * @return the <code>TagTester</code> for the tag which has the given DOM id
	 */
	public TagTester getTagById(String id)
	{
		return TagTester.createTagByAttribute(getLastResponseAsString(), "id", id);
	}

	/**
	 * Helper method for all the places where an Ajax call should submit an associated
	 * <code>Form</code>.
	 *
	 * @param component
	 *            The component the behavior is attached to
	 * @param behavior
	 *            The <code>AjaxFormSubmitBehavior</code> with the <code>Form</code> to "submit"
	 */
	private void submitAjaxFormSubmitBehavior(final Component component,
		AjaxFormSubmitBehavior behavior)
	{
		// The form that needs to be "submitted".
		Form<?> form = behavior.getForm();
		assertNotNull(form, "No form attached to the submitlink.");

		checkUsability(form, true);
		serializeFormToRequest(form);
		executeBehavior(behavior);
	}

	/**
	 * Puts all not already scheduled (e.g. via {@link FormTester#setValue(String, String)}) form
	 * component values in the post parameters for the next form submit
	 *
	 * @param form
	 *            the {@link Form} which components should be submitted
	 */
	private void serializeFormToRequest(final Form<?> form)
	{
		final MockRequestParameters postParameters = request.getPostParameters();
		final Set<String> currentParameterNamesSet = postParameters.getParameterNames();

		form.visitFormComponents(new IVisitor<FormComponent<?>, Void>()
		{
			@Override
			public void component(final FormComponent<?> formComponent, final IVisit<Void> visit)
			{
				final String inputName = formComponent.getInputName();
				if (!currentParameterNamesSet.contains(inputName))
				{
					String[] values = FormTester.getInputValue(formComponent);
					for (String value : values)
					{
						postParameters.addParameterValue(inputName, value);
					}
				}
			}
		});
	}

	/**
	 * Retrieves the content type from the response header.
	 *
	 * @return the content type from the response header
	 */
	public String getContentTypeFromResponseHeader()
	{
		String contentType = getLastResponse().getContentType();
		assertNotNull(contentType, "No Content-Type header found");
		return contentType;
	}

	/**
	 * Retrieves the content length from the response header.
	 *
	 * @return the content length from the response header
	 */
	public int getContentLengthFromResponseHeader()
	{
		String contentLength = getLastResponse().getHeader("Content-Length");
		assertNotNull(contentLength, "No Content-Length header found");
		return Integer.parseInt(contentLength);
	}

	/**
	 * Retrieves the last-modified value from the response header.
	 *
	 * @return the last-modified value from the response header
	 */
	public String getLastModifiedFromResponseHeader()
	{
		return getLastResponse().getHeader("Last-Modified");
	}

	/**
	 * Retrieves the content disposition from the response header.
	 *
	 * @return the content disposition from the response header
	 */
	public String getContentDispositionFromResponseHeader()
	{
		return getLastResponse().getHeader("Content-Disposition");
	}

	/**
	 * Rebuilds {@link ServletWebRequest} used by wicket from the mock request used to build
	 * requests. Sometimes this method is useful when changes need to be checked without processing
	 * a request.
	 */
	public void applyRequest()
	{
		Request req = newServletWebRequest();
		requestCycle.setRequest(req);
		if (useRequestUrlAsBase)
		{
			requestCycle.getUrlRenderer().setBaseUrl(req.getUrl());
		}
	}

	/**
	 *
	 * @param message
	 * @param condition
	 * @return fail with message if false
	 */
	private Result isTrue(String message, boolean condition)
	{
		if (condition)
		{
			return Result.pass();
		}
		return Result.fail(message);
	}

	/**
	 *
	 * @param message
	 * @param condition
	 * @return fail with message if true
	 */
	private Result isFalse(String message, boolean condition)
	{
		if (!condition)
		{
			return Result.pass();
		}
		return Result.fail(message);
	}

	/**
	 *
	 * @param expected
	 * @param actual
	 * @return fail with message if not equal
	 */
	protected final Result isEqual(Object expected, Object actual)
	{
		if (expected == null && actual == null)
		{
			return Result.pass();
		}
		if (expected != null && expected.equals(actual))
		{
			return Result.pass();
		}
		String message = "expected:<" + expected + "> but was:<" + actual + ">";
		return Result.fail(message);
	}

	/**
	 *
	 * @param message
	 * @param object
	 */
	private void notNull(String message, Object object)
	{
		if (object == null)
		{
			fail(message);
		}
	}

	/**
	 * Checks whether a component is visible and/or enabled before usage
	 *
	 * @param component
	 * @param throwException
	 * @return result
	 */
	protected Result checkUsability(final Component component, boolean throwException)
	{
		Result res = Result.pass();

		if (component.isVisibleInHierarchy() == false)
		{
			res = Result.fail(
				"The component is currently not visible in the hierarchy and thus you can not be used."
					+ " Component: " + component);
		}

		if (component.isEnabledInHierarchy() == false)
		{
			res = Result.fail(
				"The component is currently not enabled in the hierarchy and thus you can not be used."
					+ " Component: " + component);
		}

		if (throwException && res.wasFailed())
		{
			throw new AssertionError(res.getMessage());
		}
		return res;
	}

	/**
	 * @return request cycle
	 */
	public RequestCycle getRequestCycle()
	{
		return requestCycle;
	}

	/**
	 * @return servlet response
	 */
	public MockHttpServletResponse getResponse()
	{
		return response;
	}

	/**
	 * @return last request
	 */
	public MockHttpServletRequest getLastRequest()
	{
		return lastRequest;
	}

	/**
	 * @return true, if exceptions are exposed
	 */
	public boolean isExposeExceptions()
	{
		return exposeExceptions;
	}

	/**
	 * @param exposeExceptions
	 */
	public void setExposeExceptions(boolean exposeExceptions)
	{
		this.exposeExceptions = exposeExceptions;
	}

	/**
	 * @return useRequestUrlAsBase
	 */
	public boolean isUseRequestUrlAsBase()
	{
		return useRequestUrlAsBase;
	}

	/**
	 * @param setBaseUrl
	 */
	public void setUseRequestUrlAsBase(boolean setBaseUrl)
	{
		useRequestUrlAsBase = setBaseUrl;
	}

	/**
	 * Starts a page, a shared resource or a {@link IRequestListener} depending on what the
	 * {@link IRequestMapper}s resolve for the passed url.
	 *
	 * @param _url
	 *            the url to resolve and execute
	 */
	public void executeUrl(final String _url)
	{
		Url url = Url.parse(_url, Charset.forName(request.getCharacterEncoding()));
		transform(url);
		getRequest().setUrl(url);
		processRequest();
	}

	/**
	 * A page that is used as the automatically created page for
	 * {@link BaseWicketTester#startComponentInPage(Class)} and the other variations.
	 * <p>
	 * This page caches the generated markup so that it is available even after
	 * {@link Component#detach()} where the {@link Component#markup component's markup cache} is
	 * cleared.
	 */
	public static class StartComponentInPage extends WebPage
	{
		private transient IMarkupFragment pageMarkup = null;

		/**
		 * Construct.
		 */
		public StartComponentInPage()
		{
			setStatelessHint(false);
		}

		@Override
		public IMarkupFragment getMarkup()
		{
			IMarkupFragment calculatedMarkup = null;
			if (pageMarkup == null)
			{
				IMarkupFragment markup = super.getMarkup();
				if (markup != null && markup != Markup.NO_MARKUP)
				{
					calculatedMarkup = markup;
					pageMarkup = markup;
				}
			}
			else
			{
				calculatedMarkup = pageMarkup;
			}

			return calculatedMarkup;
		}

		/**
		 * @param markup
		 */
		public void setPageMarkup(IMarkupFragment markup)
		{
			setMarkup(markup);
			pageMarkup = markup;
		}
	}

	private static class TestPageManagerProvider implements IPageManagerProvider
	{
		@Override
		public IPageManager get()
		{
			return new MockPageManager();
		}
	}

	private static class WicketTesterServletWebResponse extends ServletWebResponse
		implements
			IMetaDataBufferingWebResponse
	{
		private List<Cookie> cookies = new ArrayList<Cookie>();

		private WicketTesterServletWebResponse(ServletWebRequest request,
			MockHttpServletResponse response)
		{
			super(request, response);
		}

		@Override
		public void addCookie(Cookie cookie)
		{
			super.addCookie(cookie);
			cookies.add(cookie);
		}

		@Override
		public void writeMetaData(WebResponse webResponse)
		{
			for (Cookie cookie : cookies)
			{
				webResponse.addCookie(cookie);
			}
		}

		@Override
		public void sendRedirect(String url)
		{
			super.sendRedirect(url);
			try
			{
				getContainerResponse().sendRedirect(url);
			}
			catch (IOException e)
			{
				throw new RuntimeException(e);
			}
		}
	}

	private class LastPageRecordingPageRendererProvider implements IPageRendererProvider
	{
		private final IPageRendererProvider delegate;

		private Page lastPage;

		private LastPageRecordingPageRendererProvider(IPageRendererProvider delegate)
		{
			this.delegate = delegate;
		}

		@Override
		public PageRenderer apply(final RenderPageRequestHandler handler)
		{
			return new PageRenderer(handler)
			{
				@Override
				public void respond(RequestCycle requestCycle)
				{
					delegate.apply(handler).respond(requestCycle);

					// WICKET-5424 record page after wrapped renderer has responded
					if (handler.getPageProvider().hasPageInstance())
					{
						Page renderedPage = (Page)handler.getPageProvider().getPageInstance();
						if (componentInPage != null && lastPage != null && renderedPage != null
							&& lastPage.getPageClass() != renderedPage.getPageClass())
						{
							// WICKET-3913: reset startComponent if a new page
							// type is rendered
							componentInPage = null;
						}
						lastRenderedPage = lastPage = renderedPage;
					}
					else
					{
						lastRenderedPage = null;
					}
				}
			};
		}
	}

	private class TestExceptionMapper implements IExceptionMapper
	{
		private final IExceptionMapper delegate;

		private TestExceptionMapper(IExceptionMapper delegate)
		{
			this.delegate = delegate;
		}

		@Override
		public IRequestHandler map(Exception e)
		{
			if (exposeExceptions)
			{
				if (e instanceof RuntimeException)
				{
					throw (RuntimeException)e;
				}
				else
				{
					throw new WicketRuntimeException(e);
				}
			}
			else
			{
				return delegate.map(e);
			}
		}
	}

	private class TestRequestCycleProvider implements IRequestCycleProvider
	{
		private final IRequestCycleProvider delegate;

		private TestRequestCycleProvider(IRequestCycleProvider delegate)
		{
			this.delegate = delegate;
		}

		@Override
		public RequestCycle apply(RequestCycleContext context)
		{
			context.setRequestMapper(new TestRequestMapper(context.getRequestMapper()));
			forcedHandler = null;
			context.setExceptionMapper(new TestExceptionMapper(context.getExceptionMapper()));
			return delegate.apply(context);
		}
	}

	private class TestRequestMapper implements IRequestMapperDelegate
	{
		private final IRequestMapper delegate;

		private TestRequestMapper(IRequestMapper delegate)
		{
			this.delegate = delegate;
		}

		@Override
		public IRequestMapper getDelegateMapper()
		{
			return delegate;
		}

		@Override
		public int getCompatibilityScore(Request request)
		{
			return delegate.getCompatibilityScore(request);
		}

		@Override
		public Url mapHandler(IRequestHandler requestHandler)
		{
			return delegate.mapHandler(requestHandler);
		}

		@Override
		public IRequestHandler mapRequest(Request request)
		{
			if (forcedHandler != null)
			{
				IRequestHandler handler = forcedHandler;
				forcedHandler = null;
				return handler;
			}
			else
			{
				return delegate.mapRequest(request);
			}
		}
	}

	public class TestFilterConfig implements FilterConfig
	{
		private final Map<String, String> initParameters = new HashMap<String, String>();

		private TestFilterConfig()
		{
			initParameters.put(WicketFilter.FILTER_MAPPING_PARAM, "/servlet/*");
		}

		@Override
		public String getFilterName()
		{
			return getClass().getName();
		}

		@Override
		public ServletContext getServletContext()
		{
			return servletContext;
		}

		@Override
		public String getInitParameter(String s)
		{
			return initParameters.get(s);
		}

		@Override
		public Enumeration<String> getInitParameterNames()
		{
			throw new UnsupportedOperationException("Not implemented");
		}
	}
}
