/*
 * 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.protocol.http.mock;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterRegistration;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRegistration;
import jakarta.servlet.ServletRegistration.Dynamic;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.SessionCookieConfig;
import jakarta.servlet.SessionTrackingMode;
import jakarta.servlet.descriptor.JspConfigDescriptor;

import org.apache.wicket.Application;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.util.cookies.CookieUtils;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.value.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Mock implementation of the servlet context for testing purposes. This implementation supports all
 * of the standard context methods except that request dispatching just indicates what is being
 * dispatched to, rather than doing the actual dispatch.
 * <p>
 * The context can be configured with a path parameter that should point to an absolute directory
 * location that represents the place where the contents of the WAR bundle are located. Setting this
 * value allows all of the resource location functionality to work as in a fully functioning web
 * application. This value is not set then not resource location functionality will work and instead
 * null will always be returned.
 *
 * @author Chris Turner
 */
public class MockServletContext implements ServletContext
{
	private static final Logger log = LoggerFactory.getLogger(MockServletContext.class);

	private final Application application;

	private final ValueMap attributes = new ValueMap();

	private final ValueMap initParameters = new ValueMap();

	private final Map<String, ServletRegistration.Dynamic> servletRegistration = new HashMap<>();

	/** Map of mime types */
	private final ValueMap mimeTypes = new ValueMap();

	private File webappRoot;

	private final SessionCookieConfig sessionCookieConfig = new SessionCookieConfig()
	{
		private boolean secure;
		private String path;
		private String name = CookieUtils.DEFAULT_SESSIONID_COOKIE_NAME;
		private int maxAge;
		private boolean httpOnly;
		private String domain;
		private String comment;

		private final Map<String, String> attributes = new HashMap<>();

		@Override
		public void setSecure(boolean secure)
		{
			this.secure = secure;
		}

		@Override
		public void setPath(String path)
		{
			this.path = path;
		}

		@Override
		public void setName(String name)
		{
			this.name = name;
		}

		@Override
		public void setMaxAge(int maxAge)
		{
			this.maxAge = maxAge;
		}

		@Override
		public void setHttpOnly(boolean httpOnly)
		{
			this.httpOnly = httpOnly;
		}

		@Override
		public void setDomain(String domain)
		{
			this.domain = domain;
		}

		@Override
		public void setComment(String comment)
		{
			this.comment = comment;
		}

		@Override
		public boolean isSecure()
		{
			return secure;
		}

		@Override
		public boolean isHttpOnly()
		{
			return httpOnly;
		}

		@Override
		public String getPath()
		{
			return path;
		}

		@Override
		public String getName()
		{
			return name;
		}

		@Override
		public int getMaxAge()
		{
			return maxAge;
		}

		@Override
		public void setAttribute(String s, String s1) {
			attributes.put(s, s1);
		}

		@Override
		public String getAttribute(String s) {
			return attributes.get(s);
		}

		@Override
		public Map<String, String> getAttributes() {
			return Map.copyOf(attributes);
		}

		@Override
		public String getDomain()
		{
			return domain;
		}

		@Override
		public String getComment()
		{
			return comment;
		}
	};

	private int sessionTimeout = 30; // in minutes
	private Charset requestCharacterEncoding = StandardCharsets.UTF_8;
	private Charset responseCharacterEncoding = StandardCharsets.UTF_8;

	/**
	 * Create the mock object. As part of the creation, the context sets the root directory where
	 * web application content is stored. This must be an ABSOLUTE directory relative to where the
	 * tests are being executed. For example: <code>System.getProperty("user.dir") +
	 * "/src/webapp"</code>
	 *
	 * @param application
	 *            The application that this context is for
	 * @param path
	 *            The path to the root of the web application
	 */
	public MockServletContext(final Application application, final String path)
	{
		this.application = application;

		webappRoot = null;
		if (path != null)
		{
			webappRoot = new File(path);
			if (!webappRoot.exists() || !webappRoot.isDirectory())
			{
				log.warn("WARNING: The webapp root directory is invalid: " + path);
				webappRoot = null;
			}
		}

		// the user app can configure specific work folder by setting -Dwicket.tester.work.folder JVM option,
		// otherwise assume we're running in maven or an eclipse project created by maven,
		// so the sessions directory will be created inside the target directory,
		// and will be cleaned up with a mvn clean

		String workFolder = System.getProperty("wicket.tester.work.folder", "target/work/");
		File file = new File(workFolder);
		try
		{
			file.mkdirs();
		}
		catch (SecurityException sx)
		{
			// not allowed to write so fallback to tmpdir
			String tmpDir = System.getProperty("java.io.tmpdir");
			file = new File(tmpDir);
		}

		attributes.put("jakarta.servlet.context.tempdir", file);

		mimeTypes.put("html", "text/html");
		mimeTypes.put("htm", "text/html");
		mimeTypes.put("css", "text/css");
		mimeTypes.put("xml", "text/xml");
		mimeTypes.put("js", "text/javascript");
		mimeTypes.put("gif", "image/gif");
		mimeTypes.put("jpg", "image/jpeg");
		mimeTypes.put("png", "image/png");
	}

	/**
	 * Add an init parameter.
	 *
	 * @param name
	 *            The parameter name
	 * @param value
	 *            The parameter value
	 */
	public void addInitParameter(final String name, final String value)
	{
		initParameters.put(name, value);
	}

	// Configuration methods

	/**
	 * Add a new recognized mime type.
	 *
	 * @param fileExtension
	 *            The file extension (e.g. "jpg")
	 * @param mimeType
	 *            The mime type (e.g. "image/jpeg")
	 */
	public void addMimeType(final String fileExtension, final String mimeType)
	{
		mimeTypes.put(fileExtension, mimeType);
	}

	/**
	 * Get an attribute with the given name.
	 *
	 * @param name
	 *            The attribute name
	 * @return The value, or null
	 */
	@Override
	public Object getAttribute(final String name)
	{
		return attributes.get(name);
	}

	/**
	 * Get all of the attribute names.
	 *
	 * @return The attribute names
	 */
	@Override
	public Enumeration<String> getAttributeNames()
	{
		return Collections.enumeration(attributes.keySet());
	}

	// ServletContext interface methods

	/**
	 * Get the context for the given URL path
	 *
	 * @param name
	 *            The url path
	 * @return Always returns this
	 */
	@Override
	public ServletContext getContext(String name)
	{
		return this;
	}

	/**
	 * Get the init parameter with the given name.
	 *
	 * @param name
	 *            The name
	 * @return The parameter, or null if no such parameter
	 */
	@Override
	public String getInitParameter(final String name)
	{
		return initParameters.getString(name);
	}

	/**
	 * Get the name of all of the init parameters.
	 *
	 * @return The init parameter names
	 */
	@Override
	public Enumeration<String> getInitParameterNames()
	{
		return Collections.enumeration(initParameters.keySet());
	}

	@Override
	public boolean setInitParameter(String name, String value)
	{
		return false;
	}

	/**
	 * Get the mime type for the given file. Uses a hardcoded map of mime types set at
	 * Initialization time.
	 *
	 * @param name
	 *            The name to get the mime type for
	 * @return The mime type
	 */
	@Override
	public String getMimeType(final String name)
	{
		int index = name.lastIndexOf('.');
		if (index == -1 || index == (name.length() - 1))
		{
			return null;
		}
		else
		{
			return mimeTypes.getString(name.substring(index + 1));
		}
	}

	@Override
	public int getMajorVersion()
	{
		return 6;
	}

	@Override
	public int getMinorVersion()
	{
		return 0;
	}

	@Override
	public int getEffectiveMajorVersion()
	{
		return 6;
	}

	@Override
	public int getEffectiveMinorVersion()
	{
		return 0;
	}

	/**
	 * Wicket does not use the RequestDispatcher, so this implementation just returns a dummy value.
	 *
	 * @param name
	 *            The name of the servlet or JSP
	 * @return The dispatcher
	 */
	@Override
	public RequestDispatcher getNamedDispatcher(final String name)
	{
		return getRequestDispatcher(name);
	}

	/**
	 * Get the real file path of the given resource name.
	 *
	 * @param name
	 *            The name
	 * @return The real path or null
	 */
	@Override
	public String getRealPath(String name)
	{
		try {
			URL url = getResource(name);
			if (url != null) {
				// WICKET-6755 do not use url.getFile() as it does not properly decode the path
				return new File(url.toURI()).getAbsolutePath();
			}
		} catch (IOException | URISyntaxException e) {
			log.error(e.getMessage(), e);
		}
		return null;
	}

	/**
	 * Wicket does not use the RequestDispatcher, so this implementation just returns a dummy value.
	 *
	 * @param name
	 *            The name of the resource to get the dispatcher for
	 * @return The dispatcher
	 */
	@Override
	public RequestDispatcher getRequestDispatcher(final String name)
	{
		return new RequestDispatcher()
		{
			@Override
			public void forward(ServletRequest servletRequest, ServletResponse servletResponse)
				throws IOException
			{
				servletResponse.getWriter().write("FORWARD TO RESOURCE: " + name);
			}

			@Override
			public void include(ServletRequest servletRequest, ServletResponse servletResponse)
				throws IOException
			{
				servletResponse.getWriter().write("INCLUDE OF RESOURCE: " + name);
			}
		};
	}

	/**
	 * Get the URL for a particular resource that is relative to the web app root directory.
	 *
	 * @param name
	 *            The name of the resource to get
	 * @return The resource, or null if resource not found
	 * @throws MalformedURLException
	 *             If the URL is invalid
	 */
	@Override
	public URL getResource(String name) throws MalformedURLException
	{
		if (name.startsWith("/"))
		{
			name = name.substring(1);
		}

		if (webappRoot != null)
		{
			File f = new File(webappRoot, name);
			if (f.exists())
			{
				return f.toURI().toURL();
			}
		}

		return getClass().getClassLoader().getResource("META-INF/resources/" + name);
	}

	/**
	 * Get an input stream for a particular resource that is relative to the web app root directory.
	 *
	 * @param name
	 *            The name of the resource to get
	 * @return The input stream for the resource, or null of resource is not found
	 */
	@Override
	public InputStream getResourceAsStream(String name)
	{
		try {
			URL url = getResource(name);
			if (url != null) {
				return url.openStream();
			}
		} catch (IOException e) {
			log.error(e.getMessage(), e);
		}
		return null;
	}

	/**
	 * Get the resource paths starting from the web app root directory and then relative to the the
	 * given name.
	 *
	 * @param name
	 *            The starting name
	 * @return The set of resource paths at this location
	 */
	@Override
	public Set<String> getResourcePaths(String name)
	{
		if (webappRoot == null)
		{
			return new HashSet<String>();
		}

		if (name.startsWith("/"))
		{
			name = name.substring(1);
		}
		if (name.endsWith("/"))
		{
			name = name.substring(0, name.length() - 1);
		}
		String[] elements = null;
		if (name.trim().length() == 0)
		{
			elements = new String[0];
		}
		else
		{
			elements = Strings.split(name, '/');
		}

		File current = webappRoot;
		for (String element : elements)
		{
			File[] files = current.listFiles();
			boolean match = false;
			if (files != null)
			{
				for (File file : files)
				{
					if (file.getName().equals(element) && file.isDirectory())
					{
						current = file;
						match = true;
						break;
					}
				}
			}
			if (!match)
			{
				return null;
			}
		}

		File[] files = current.listFiles();
		Set<String> result = new HashSet<>();
		if (files != null)
		{
			int stripLength = webappRoot.getPath().length();
			for (File file : files)
			{
				String s = file.getPath().substring(stripLength).replace('\\', '/');
				if (file.isDirectory())
				{
					s = s + "/";
				}
				result.add(s);
			}
		}
		return result;
	}

	/**
	 * Get the server info.
	 *
	 * @return The server info
	 */
	@Override
	public String getServerInfo()
	{
		return "Wicket Mock Test Environment v1.0";
	}

	/**
	 * Return the name of the servlet context.
	 *
	 * @return The name
	 */
	@Override
	public String getServletContextName()
	{
		return application.getName();
	}

	@Override
	public ServletRegistration.Dynamic addServlet(String servletName, String className)
	{
		try
		{
			return addServlet(servletName, Class.forName(className).asSubclass(Servlet.class));
		}
		catch (ClassNotFoundException e)
		{
			throw new WicketRuntimeException(e);
		}
	}

	@Override
	public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
	{
		Dynamic mockRegistration = (Dynamic)Proxy.newProxyInstance(Dynamic.class.getClassLoader(),
			new Class<?>[]{Dynamic.class}, new MockedServletRegistationHandler(servletName));

		servletRegistration.put(servletName, mockRegistration);

		return mockRegistration;
	}

	@Override
	public ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass)
	{
		try
		{
			return addServlet(servletName, servletClass.getDeclaredConstructor().newInstance());
		}
		catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e)
		{
			throw new WicketRuntimeException(e);
		}
	}

	@Override
	public Dynamic addJspFile(String s, String s1)
	{
		return null;
	}

	@Override
	public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException
	{
		try
		{
			return clazz.getDeclaredConstructor().newInstance();
		}
		catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e)
		{
			throw new WicketRuntimeException(e);
		}
	}

	@Override
	public ServletRegistration getServletRegistration(String servletName)
	{
		return servletRegistration.get(servletName);
	}

	@Override
	public Map<String, ? extends ServletRegistration> getServletRegistrations()
	{
		return servletRegistration;
	}

	@Override
	public FilterRegistration.Dynamic addFilter(String filterName, String className)
	{
		return null;
	}

	@Override
	public FilterRegistration.Dynamic addFilter(String filterName, Filter filter)
	{
		return null;
	}

	@Override
	public FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass)
	{
		return null;
	}

	@Override
	public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException
	{
		return null;
	}

	@Override
	public FilterRegistration getFilterRegistration(String filterName)
	{
		return null;
	}

	@Override
	public Map<String, ? extends FilterRegistration> getFilterRegistrations()
	{
		return null;
	}

	@Override
	public SessionCookieConfig getSessionCookieConfig()
	{
		return sessionCookieConfig;
	}

	@Override
	public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes)
	{
	}

	@Override
	public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
	{
		return EnumSet.of(SessionTrackingMode.COOKIE);
	}

	@Override
	public Set<SessionTrackingMode> getEffectiveSessionTrackingModes()
	{
		return getDefaultSessionTrackingModes();
	}

	@Override
	public void addListener(String className)
	{
	}

	@Override
	public <T extends EventListener> void addListener(T t)
	{
	}

	@Override
	public void addListener(Class<? extends EventListener> listenerClass)
	{
	}

	@Override
	public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException
	{
		return null;
	}

	@Override
	public JspConfigDescriptor getJspConfigDescriptor()
	{
		return null;
	}

	@Override
	public ClassLoader getClassLoader()
	{
		return null;
	}

	@Override
	public void declareRoles(String... roleNames)
	{
	}

	@Override
	public String getVirtualServerName()
	{
		return "WicketTester 8.x";
	}

	@Override
	public int getSessionTimeout()
	{
		return sessionTimeout;
	}

	@Override
	public void setSessionTimeout(int sessionTimeout)
	{
		this.sessionTimeout = sessionTimeout;
	}

	@Override
	public String getRequestCharacterEncoding()
	{
		return requestCharacterEncoding.name();
	}

	@Override
	public void setRequestCharacterEncoding(String requestCharacterEncoding)
	{
		this.requestCharacterEncoding = Charset.forName(requestCharacterEncoding);
	}

	@Override
	public String getResponseCharacterEncoding()
	{
		return responseCharacterEncoding.name();
	}

	@Override
	public void setResponseCharacterEncoding(String responseCharacterEncoding)
	{
		this.responseCharacterEncoding = Charset.forName(responseCharacterEncoding);
	}

	/**
	 * As part of testing we always log to the console.
	 *
	 * @param msg
	 *            The message to log
	 */
	@Override
	public void log(String msg)
	{
		log.info(msg);
	}

	/**
	 * As part of testing we always log to the console.
	 *
	 * @param msg
	 *            The message to log
	 * @param cause
	 *            The cause exception
	 */
	@Override
	public void log(String msg, Throwable cause)
	{
		log.error(msg, cause);
	}

	/**
	 * Remove an attribute with the given name.
	 *
	 * @param name
	 *            The name
	 */
	@Override
	public void removeAttribute(final String name)
	{
		attributes.remove(name);
	}

	/**
	 * Set an attribute.
	 *
	 * @param name
	 *            The name of the attribute
	 * @param o
	 *            The value
	 */
	@Override
	public void setAttribute(final String name, final Object o)
	{
		attributes.put(name, o);
	}

	/**
	 * @return context path
	 */
	@Override
	public String getContextPath()
	{
		return "";
	}


	/**
	 * Invocation handler for proxy interface of {@link jakarta.servlet.ServletRegistration.Dynamic}.
	 * This class intercepts invocation for method {@link jakarta.servlet.ServletRegistration.Dynamic#getMappings}
	 * and returns the servlet name.
	 *
	 * @author andrea del bene
	 *
	 */
	class MockedServletRegistationHandler implements InvocationHandler
	{

		private final Collection<String> servletName;

		public MockedServletRegistationHandler(String servletName)
		{
			this.servletName = Arrays.asList(servletName);
		}

		@Override
		public Object invoke(Object object, Method method, Object[] args) throws Throwable
		{
			if (method.getName().equals("getMappings"))
			{
				return servletName;
			}

			return null;
		}
	}
}
