/*******************************************************************************
 * Copyright (c) 2010, 2021 SAP AG 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:
 *     SAP AG - initial API and implementation
 *******************************************************************************/
package org.eclipse.tycho.test.TYCHO279HttpProxy;

import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.apache.maven.it.Verifier;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.bio.SocketConnector;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.tycho.test.AbstractTychoIntegrationTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.sonatype.jettytestsuite.ProxyServer;
import org.sonatype.jettytestsuite.proxy.MonitorableProxyServlet;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class ProxySupportTest extends AbstractTychoIntegrationTest {

	private static final String TEST_BASEDIR = "/TYCHO279HttpProxy";

	private static final String PATH = "/test/";

	private Server httpServer;

	private int httpServerPort;

	private ProxyServer proxyServer;

	private int proxyPort;

	private MonitorableProxyServlet proxyServlet;

	private File baseDir;

	private File settings;

	@Before
	public void setup() throws Exception {
		baseDir = new File(getVerifier(TEST_BASEDIR).getBasedir());
		settings = new File(baseDir, "settings.xml");
		startHttpServer();
	}

	@After
	public void tearDown() throws Exception {
		httpServer.stop();
		httpServer.join();
		proxyServer.getServer().stop();
		proxyServer.getServer().join();
	}

	@Test
	public void testActiveProxy() throws Exception {
		startHttpProxyServer(false, null, null);
		Verifier verifier = getVerifier(TEST_BASEDIR, false);
		configureProxyInSettingsXml(true, null, null);
		replaceSettingsArg(verifier);
		verifier.getSystemProperties().setProperty("p2.repo", getP2RepoUrl());
		verifier.executeGoal("package");
		verifier.verifyErrorFreeLog();
		List<String> accessedUris = proxyServlet.getAccessedUris();
		assertTrue("proxy was not accessed", accessedUris.size() > 0);
		String expectedUri = getP2RepoUrl() + "content.xml";
		assertTrue("URL " + expectedUri + " was not accessed via proxy", accessedUris.contains(expectedUri));
	}

	@Test
	public void testProxyWithAuthenticationEncryptedPassword() throws Exception {
		final String proxyUser = "foo";
		final String proxyPassword = "bar";
		final String proxyPasswordEncrypted = "{kw/hJWrJhk0H93GVJNM/IbE7RQot5UfTKlAVLDfyDG8=}";
		startHttpProxyServer(true, proxyUser, proxyPassword);
		Verifier verifier = getVerifier(TEST_BASEDIR, false);
		configureProxyInSettingsXml(true, proxyUser, proxyPasswordEncrypted);
		replaceSettingsArg(verifier);
		Properties systemProperties = verifier.getSystemProperties();
		systemProperties.setProperty("p2.repo", getP2RepoUrl());
		systemProperties.setProperty("settings.security", new File(baseDir, "settings-security.xml").getAbsolutePath());
		verifier.executeGoal("package");
		verifier.verifyErrorFreeLog();
		List<String> accessedUris = proxyServlet.getAccessedUris();
		assertTrue("proxy was not accessed", accessedUris.size() > 0);
		String expectedUri = getP2RepoUrl() + "content.xml";
		assertTrue("URL " + expectedUri + " was not accessed via proxy", accessedUris.contains(expectedUri));
	}

	@Test
	public void testInactiveProxy() throws Exception {
		startHttpProxyServer(false, null, null);
		Verifier verifier = getVerifier(TEST_BASEDIR, false);
		configureProxyInSettingsXml(false, null, null);
		replaceSettingsArg(verifier);
		verifier.getSystemProperties().setProperty("p2.repo", getP2RepoUrl());
		verifier.executeGoal("package"); // build fails
		List<String> accessedUris = proxyServlet.getAccessedUris();
		assertTrue("proxy was accessed although not active. Accessed URIs: " + accessedUris, accessedUris.isEmpty());
	}

	private void startHttpProxyServer(boolean useAuthentication, String user, String password) throws Exception {
		proxyServer = new ProxyServer();
		proxyPort = findFreePort();
		proxyServer.setPort(proxyPort);
		proxyServer.initialize();

		Map<String, String> authMap = new HashMap<>();
		if (useAuthentication) {
			authMap.put(user, password);
		}
		proxyServlet = proxyServer.getProxyServlet();
		proxyServlet.setAuthentications(authMap);
		proxyServlet.setUseAuthentication(useAuthentication);

		proxyServer.start();
	}

	private void startHttpServer() throws Exception {
		httpServer = new Server();
		Connector connector = new SocketConnector();
		httpServerPort = findFreePort();
		connector.setPort(httpServerPort);
		httpServer.addConnector(connector);
		ContextHandler context = new ContextHandler(httpServer, "/test");
		context.setResourceBase(new File(baseDir, "repo").getAbsolutePath());
		context.setHandler(new ResourceHandler());
		httpServer.setHandler(context);
		httpServer.start();
	}

	private static int findFreePort() throws IOException {
		try (ServerSocket serverSocket = new ServerSocket(0)) {
			return serverSocket.getLocalPort();
		}
	}

	private String getP2RepoUrl() {
		return "http://localhost:" + httpServerPort + PATH;
	}

	private void replaceSettingsArg(Verifier verifier) throws IOException {
		List<String> cliOptions = verifier.getCliOptions();
		for (Iterator<String> iterator = cliOptions.iterator(); iterator.hasNext();) {
			String arg = iterator.next();
			if (arg.startsWith("-s ")) {
				iterator.remove();
			}
		}
		cliOptions.add("-s " + settings.getCanonicalPath());
	}

	private void configureProxyInSettingsXml(boolean isProxyActive, String user, String password) throws Exception {
		Document dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(settings);
		XPath xpath = XPathFactory.newInstance().newXPath();
		XPathExpression proxyExpr = xpath.compile("/settings/proxies/proxy");
		Element proxyNode = (Element) proxyExpr.evaluate(dom.getDocumentElement(), XPathConstants.NODE);
		{
			XPathExpression portExpr = xpath.compile("/settings/proxies/proxy/port");
			Element node = (Element) portExpr.evaluate(dom.getDocumentElement(), XPathConstants.NODE);
			node.setTextContent(String.valueOf(proxyPort));
		}
		{
			XPathExpression activeExpr = xpath.compile("/settings/proxies/proxy/active");
			Element activeNode = (Element) activeExpr.evaluate(dom.getDocumentElement(), XPathConstants.NODE);
			activeNode.setTextContent(String.valueOf(isProxyActive));
		}
		{
			XPathExpression userExpr = xpath.compile("/settings/proxies/proxy/username");
			Element userNode = (Element) userExpr.evaluate(dom.getDocumentElement(), XPathConstants.NODE);
			updateNodeValue("username", userNode, user, dom, proxyNode);
		}
		{
			XPathExpression passwordExpr = xpath.compile("/settings/proxies/proxy/password");
			Element passwordNode = (Element) passwordExpr.evaluate(dom.getDocumentElement(), XPathConstants.NODE);
			updateNodeValue("password", passwordNode, password, dom, proxyNode);
		}
		Transformer xslTransformer = TransformerFactory.newInstance().newTransformer();
		xslTransformer.transform(new DOMSource(dom.getDocumentElement()), new StreamResult(settings));
	}

	private static void updateNodeValue(String nodeName, Element node, String newValue, Document dom,
			Element parentNode) {
		if (newValue != null) {
			if (node == null) {
				node = dom.createElement(nodeName);
				parentNode.appendChild(node);
			}
			node.setTextContent(newValue);
		} else {
			if (node != null) {
				parentNode.removeChild(node);
			}
		}
	}

}
