/*******************************************************************************
 * Copyright (c) 2011, 2021 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Raymond Augé <raymond.auge@liferay.com> - Bug 436698
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 567831
 *******************************************************************************/
package org.eclipse.equinox.http.servlet.tests.util;

import static org.junit.Assert.assertEquals;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

/*
 * The ServletRequestAdvisor is responsible for composing URLs and using them
 * to performing servlet requests.
 */
public class ServletRequestAdvisor extends Object {
	private final String contextPath;
	private final String port;
	private final String ksPath;
	private final String ksPassword;
	private final int timeout;

	public ServletRequestAdvisor(String port, String contextPath) {
		this(port, contextPath, null, null, 150);
	}

	public ServletRequestAdvisor(String port, String contextPath, int timeout) {
		this(port, contextPath, null, null, timeout);
	}

	public ServletRequestAdvisor(String port, String contextPath, String ksPath, String ksPassword) {
		this(port, contextPath, ksPath, ksPassword, 150);
	}

	public ServletRequestAdvisor(String port, String contextPath, String ksPath, String ksPassword, int timeout) {
		super();
		if (port == null)
		{
			throw new IllegalArgumentException("port must not be null"); //$NON-NLS-1$
		}
		this.port = port;
		this.contextPath = contextPath;
		this.ksPath = ksPath;
		this.ksPassword = ksPassword;
		this.timeout = timeout * 100000;
	}

	private String createUrlSpec(String value, boolean isHttps) {
		StringBuilder buffer = new StringBuilder(100);
		String protocol = "http://"; //$NON-NLS-1$
		if (isHttps) {
			protocol = "https://";
		}
		String host = "localhost"; //$NON-NLS-1$
		buffer.append(protocol);
		buffer.append(host);
		buffer.append(':');
		buffer.append(port);
		buffer.append(contextPath);
		if (value != null) {
			buffer.append('/');
			buffer.append(value);
		}
		return buffer.toString();

	}

	private String createUrlSpec(String value) {
		return createUrlSpec(value, false);
	}

	private String drain(InputStream stream) throws IOException {
		byte[] bytes = new byte[100];
		StringBuilder buffer = new StringBuilder(500);
		int length;
		while ((length = stream.read(bytes)) != -1) {
			String chunk = new String(bytes, 0, length);
			buffer.append(chunk);
		}
		return buffer.toString();
	}

	private void log(String message) {
		String value = this + ": " + message; //$NON-NLS-1$
		System.out.println(value);
	}

	public String request(String value) throws IOException {
		String spec = createUrlSpec(value);
		log("Requesting " + spec); //$NON-NLS-1$
		URL url = new URL(spec);

		HttpURLConnection connection = (HttpURLConnection)url.openConnection();

		connection.setInstanceFollowRedirects(false);
		connection.setConnectTimeout(timeout);
		connection.setReadTimeout(timeout);
		connection.connect();

		try (InputStream stream = connection.getInputStream()){
			return drain(stream);
		}
	}

	public String requestHttps(String value) throws Exception {
		String spec = createUrlSpec(value, true);
		log("Requesting " + spec); //$NON-NLS-1$
		URL url = new URL(spec);
		SSLContext sslContext = SSLContext.getInstance("SSL");
		initializeSSLContext(sslContext, ksPath, ksPassword);

		HttpsURLConnection httpsConn = (HttpsURLConnection)url.openConnection();
		httpsConn.setSSLSocketFactory(sslContext.getSocketFactory());
		httpsConn.setRequestMethod("GET");
		httpsConn.setDoOutput(false);
		httpsConn.setDoInput(true);
		httpsConn.setConnectTimeout(timeout);
		httpsConn.setReadTimeout(timeout);
		httpsConn.connect();

		assertEquals("Request to the url " + spec + " was not successful", 200 , httpsConn.getResponseCode());
		try (InputStream stream = httpsConn.getInputStream()) {
			return drain(stream);
		}
	}

	private void initializeSSLContext(SSLContext sslContext, String ksPath, String ksPassword) throws Exception {
		KeyManager keyManagers[] = null;
		if (ksPath != null) {
			KeyManagerFactory kmFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
			File ksFile = new File(ksPath);
			KeyStore keyStore = KeyStore.getInstance("JKS");

			try(InputStream ksStream = new FileInputStream(ksFile)){
				keyStore.load(ksStream, ksPassword.toCharArray());
				kmFactory.init(keyStore, ksPassword.toCharArray());
				keyManagers = kmFactory.getKeyManagers();
			}
		}

		TrustManager[] trustManagers = getTrustManager();

		sslContext.init(keyManagers, trustManagers, null);

	}

	private TrustManager[] getTrustManager() {
		TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
			@Override
			public java.security.cert.X509Certificate[] getAcceptedIssuers() {
				return null;
			}

			@Override
			public void checkClientTrusted(
										java.security.cert.X509Certificate[] certs, String authType) {}

			@Override
			public void checkServerTrusted(
										java.security.cert.X509Certificate[] certs, String authType) {}
		} };

		return trustAllCerts;
	}

	public Map<String, List<String>> request(String value, Map<String, List<String>> headers) throws IOException {
		String spec = createUrlSpec(value);
		log("Requesting " + spec); //$NON-NLS-1$
		URL url = new URL(spec);
		HttpURLConnection connection = (HttpURLConnection)url.openConnection();

		connection.setInstanceFollowRedirects(false);
		connection.setConnectTimeout(timeout);
		connection.setReadTimeout(timeout);

		if (headers != null) {
			for(Map.Entry<String, List<String>> entry : headers.entrySet()) {
				for(String entryValue : entry.getValue()) {
					connection.setRequestProperty(entry.getKey(), entryValue);
				}
			}
		}

		int responseCode = connection.getResponseCode();

		Map<String, List<String>> map = new HashMap<>(connection.getHeaderFields());
		map.put("responseCode", Collections.singletonList(String.valueOf(responseCode)));

		InputStream stream;

		if (responseCode >= 400) {
			stream = connection.getErrorStream();
		}
		else {
			stream = connection.getInputStream();
		}

		try {
			String drainedStream = drain(stream);
			map.put("responseBody", Arrays.asList(drainedStream));
		} catch (IOException e){
			map.put("responseBody", Arrays.asList(e.getMessage()));
		} finally {
			stream.close();
		}
		return map;
	}

	public Map<String, List<String>> eventSource(String value, Map<String, List<String>> headers, final EventHandler handler) throws IOException {
		String spec = createUrlSpec(value);
		log("Requesting " + spec); //$NON-NLS-1$
		URL url = new URL(spec);
		HttpURLConnection connection = (HttpURLConnection)url.openConnection();

		connection.setChunkedStreamingMode(0);
		connection.setDoOutput(true);
		//connection.setRequestProperty("Connection", "Close");
		connection.setInstanceFollowRedirects(false);
		connection.setConnectTimeout(timeout);
		connection.setReadTimeout(timeout);

		if (headers != null) {
			for(Map.Entry<String, List<String>> entry : headers.entrySet()) {
				for(String entryValue : entry.getValue()) {
					connection.setRequestProperty(entry.getKey(), entryValue);
				}
			}
		}

		int responseCode = connection.getResponseCode();

		Map<String, List<String>> map = new HashMap<>(connection.getHeaderFields());
		map.put("responseCode", Collections.singletonList(String.valueOf(responseCode)));

		InputStream stream;

		if (responseCode >= 400) {
			stream = connection.getErrorStream();
		}
		else {
			stream = connection.getInputStream();
		}

		handler.open(stream);

		return map;
	}

	public Map<String, List<String>> upload(String value, Map<String, List<Object>> headers) throws IOException {
		return upload(value, headers, null);
	}
	
	public Map<String, List<String>> upload(String value, Map<String, List<Object>> headers, Map<String, Object> formFields) throws IOException {
		String spec = createUrlSpec(value);
		log("Requesting " + spec); //$NON-NLS-1$
		URL url = new URL(spec);
		HttpURLConnection connection = (HttpURLConnection)url.openConnection();

		connection.setInstanceFollowRedirects(false);
		connection.setConnectTimeout(timeout);
		connection.setReadTimeout(timeout);
		connection.setUseCaches(false);

		if (headers != null) {
			if (headers.containsKey("method")) {
				String method = (String)headers.remove("method").get(0);
				connection.setRequestMethod(method);
			}

			for(Map.Entry<String, List<Object>> entry : headers.entrySet()) {
				for(Object entryValue : entry.getValue()) {
					if (entryValue instanceof String) {
						if (entry.getKey().equals("x-www-form-urlencoded")) {
							postFormURLEncoded(connection, (String)entryValue);
						}
						else {
							String property = connection.getRequestProperty(entry.getKey());
							if (property == null) {
								connection.setRequestProperty(entry.getKey(), (String)entryValue);
							} else {
								connection.setRequestProperty(entry.getKey(), property + "," + entryValue);
							}
						}
					}
					else if (entryValue instanceof URL) {
						uploadFileConnection(connection, entry.getKey(), (URL)entryValue, formFields);
					}
					else {
						throw new IllegalArgumentException("only supports strings and files");
					}
				}
			}
		}

		int responseCode = connection.getResponseCode();

		Map<String, List<String>> map = new HashMap<>(connection.getHeaderFields());
		map.put("responseCode", Collections.singletonList(String.valueOf(responseCode)));

		InputStream stream;

		if (responseCode >= 400) {
			stream = connection.getErrorStream();
		}
		else {
			stream = connection.getInputStream();
		}

		try {
			map.put("responseBody", Arrays.asList(drain(stream)));
			return map;
		} finally {
			stream.close();
		}
	}

	private void postFormURLEncoded(HttpURLConnection connection, String param)
		throws IOException {

		byte[] bytes = param.getBytes(StandardCharsets.UTF_8);

		connection.setDoOutput(true);
		connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
		//connection.setRequestProperty("Content-Length", Integer.toString(bytes.length));

		try(DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
			wr.write(bytes);
			wr.flush();
		}
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void uploadFileConnection(HttpURLConnection connection, String param, URL file, Map<String, Object> formFields)
		throws IOException {

		String fileName = file.getPath();
		fileName = fileName.substring(fileName.lastIndexOf("/") + 1);
		connection.setDoOutput(true);

		String boundary = Long.toHexString(System.currentTimeMillis());
		String CRLF = "\r\n";
		connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);

		try (InputStream input = file.openStream();
				OutputStream output = connection.getOutputStream();
				PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8), true);) {

			// add optional parameter
			if (formFields != null) {
				formFields.entrySet().forEach(entry -> {
					if (entry.getValue() instanceof Collection) {
						((Collection)entry.getValue()).forEach(value -> {
							writer.append("--" + boundary);
							writer.append(CRLF);
							writer.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"");
							writer.append(CRLF);
							writer.append("Content-Type: text/plain; charset=UTF-8");
							writer.append(CRLF);
							writer.append(CRLF);
							writer.append((String) value);
							writer.append(CRLF);
							writer.flush();
						});
					} else {
						writer.append("--" + boundary);
						writer.append(CRLF);
						writer.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"");
						writer.append(CRLF);
						writer.append("Content-Type: text/plain; charset=UTF-8");
						writer.append(CRLF);
						writer.append(CRLF);
						writer.append((String) entry.getValue());
						writer.append(CRLF);
						writer.flush();
					}
				});
			}
			
			writer.append("--" + boundary);
			writer.append(CRLF);
			writer.append("Content-Disposition: form-data; name=\"");
			writer.append(param);
			writer.append("\"; filename=\"");
			writer.append(fileName);
			writer.append("\"");
			writer.append(CRLF);
			writer.append("Content-Type: ");
			String contentType = URLConnection.guessContentTypeFromName(fileName);
			writer.append(contentType);
			writer.append(CRLF);
			if (!contentType.startsWith("text/")) {
				writer.append("Content-Transfer-Encoding: binary");
				writer.append(CRLF);
			}
			writer.append(CRLF);
			writer.flush();

			byte[] buf = new byte[64];
			int c = 0;
			while ((c = input.read(buf, 0, buf.length)) > 0) {
				output.write(buf, 0, c);
				output.flush();
			}

			output.flush(); // Important before continuing with writer!
			writer.append(CRLF); // CRLF is important! It indicates end of boundary.
			writer.flush();

			// End of multipart/form-data.
			writer.append("--" + boundary + "--");
			writer.append(CRLF);
			writer.flush();
		}
	}

}
