package aQute.bnd.deployer.http;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.SSLHandshakeException;

import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;

import aQute.bnd.service.url.TaggedData;
import aQute.lib.io.IO;
import junit.framework.TestCase;
import test.http.ETaggingResourceHandler;

public class HttpConnectorTest extends TestCase {

	private static final String	LOCALHOST		= "127.0.0.1";

	private static int			HTTP_PORT		= 0;
	private static int			HTTPS_PORT		= 0;

	private static final String	RESOURCE_BASE	= "testdata/http";
	private static final String	SECURED_PATH	= "/securebundles/*";

	private static final String	USER_ROLE_FILE	= "testdata/jetty-users.properties";
	private static final String	KEYSTORE_PATH	= "testdata/example.keystore";
	private static final String	KEYSTORE_PASS	= "opensesame";

	private static final String	EXPECTED_ETAG	= "64035a95";

	private static Server		jetty;

	private static String getUrl(boolean http) {
		if (http) {
			return "http://127.0.0.1:" + HTTP_PORT + "/";
		}
		return "https://127.0.0.1:" + HTTPS_PORT + "/";
	}

	@Override
	protected void setUp() throws Exception {
		File tmpFile = File.createTempFile("cache", ".tmp");
		tmpFile.deleteOnExit();

		jetty = startJetty();
	}

	@Override
	protected void tearDown() throws Exception {
		jetty.stop();
	}

	private static Server startJetty() throws Exception {
		Server server = new Server();

		// Create the login service
		String REQUIRED_ROLE = "users";
		HashLoginService loginSvc = new HashLoginService(REQUIRED_ROLE, USER_ROLE_FILE);
		server.addBean(loginSvc);

		// Start HTTP and HTTPS connectors
		SelectChannelConnector httpConnector = new SelectChannelConnector();
		httpConnector.setPort(0);
		httpConnector.setHost(LOCALHOST);
		server.addConnector(httpConnector);

		SslSelectChannelConnector sslConnector = new SslSelectChannelConnector();
		sslConnector.setPort(0);
		sslConnector.setHost(LOCALHOST);
		SslContextFactory contextFactory = sslConnector.getSslContextFactory();
		contextFactory.setKeyStorePath(KEYSTORE_PATH);
		contextFactory.setKeyStorePassword(KEYSTORE_PASS);
		server.addConnector(sslConnector);

		// Create the resource handler to serve files
		ResourceHandler resourceHandler = new ETaggingResourceHandler();
		resourceHandler.setResourceBase(RESOURCE_BASE);
		resourceHandler.setDirectoriesListed(true);

		// Setup user role constraints
		Constraint constraint = new Constraint();
		constraint.setName(Constraint.__BASIC_AUTH);
		constraint.setRoles(new String[] {
				REQUIRED_ROLE
		});
		constraint.setAuthenticate(true);

		// Map constraints to the secured directory
		ConstraintMapping cm = new ConstraintMapping();
		cm.setConstraint(constraint);
		cm.setPathSpec(SECURED_PATH);

		// Setup the constraint handler
		ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
		securityHandler.setAuthMethod("BASIC");
		securityHandler.setHandler(resourceHandler);
		securityHandler.setLoginService(loginSvc);
		securityHandler.setConstraintMappings(new ConstraintMapping[] {
				cm
		});

		// Finally!! Start the server
		server.setHandler(securityHandler);
		server.start();

		while (!server.isRunning()) {
			Thread.sleep(10);
		}

		HTTP_PORT = httpConnector.getLocalPort();
		HTTPS_PORT = sslConnector.getLocalPort();
		assertNotSame(Integer.valueOf(0), Integer.valueOf(HTTP_PORT));
		assertNotSame(Integer.valueOf(-1), Integer.valueOf(HTTP_PORT));
		assertNotSame(Integer.valueOf(0), Integer.valueOf(HTTPS_PORT));
		assertNotSame(Integer.valueOf(-1), Integer.valueOf(HTTPS_PORT));
		assertNotSame(Integer.valueOf(HTTP_PORT), Integer.valueOf(HTTPS_PORT));

		return server;
	}

	public static void testConnectTagged() throws Exception {
		DefaultURLConnector connector = new DefaultURLConnector();

		TaggedData data = connector.connectTagged(new URL(getUrl(true) + "bundles/dummybundle.jar"));
		assertNotNull("Data should be non-null because ETag not provided", data);
		data.getInputStream().close();
		assertEquals("ETag is incorrect", EXPECTED_ETAG, data.getTag());
	}

	public static void testConnectKnownTag() throws Exception {
		DefaultURLConnector connector = new DefaultURLConnector();

		TaggedData data = connector.connectTagged(new URL(getUrl(true) + "bundles/dummybundle.jar"), EXPECTED_ETAG);
		assertNull("Data should be null since ETag not modified.", data);
	}

	public static void testConnectTagModified() throws Exception {
		DefaultURLConnector connector = new DefaultURLConnector();

		TaggedData data = connector.connectTagged(new URL(getUrl(true) + "bundles/dummybundle.jar"), "00000000");
		assertNotNull("Data should be non-null because ETag was different", data);
		data.getInputStream().close();
		assertEquals("ETag is incorrect", EXPECTED_ETAG, data.getTag());
	}

	public static void testConnectHTTPS() throws Exception {
		DefaultURLConnector connector = new DefaultURLConnector();
		Map<String,String> config = new HashMap<String,String>();
		config.put(HttpsUtil.PROP_DISABLE_SERVER_CERT_VERIFY, "true");
		connector.setProperties(config);

		InputStream stream = connector.connect(new URL(getUrl(false) + "bundles/dummybundle.jar"));
		assertNotNull(stream);
		stream.close();
	}

	public static void testConnectHTTPSBadCerficate() throws Exception {
		DefaultURLConnector connector = new DefaultURLConnector();

		InputStream stream = null;
		try {
			stream = connector.connect(new URL(getUrl(false) + "bundles/dummybundle.jar"));
			fail("Expected SSLHandsakeException");
		} catch (SSLHandshakeException e) {
			// expected
		} finally {
			if (stream != null)
				IO.close(stream);
		}
	}

	public static void testConnectTaggedHTTPS() throws Exception {
		DefaultURLConnector connector = new DefaultURLConnector();
		Map<String,String> config = new HashMap<String,String>();
		config.put(HttpsUtil.PROP_DISABLE_SERVER_CERT_VERIFY, "true");
		connector.setProperties(config);

		TaggedData data = connector.connectTagged(new URL(getUrl(false) + "bundles/dummybundle.jar"));
		assertNotNull(data);
		data.getInputStream().close();
	}

	public static void testConnectTaggedHTTPSBadCerficate() throws Exception {
		DefaultURLConnector connector = new DefaultURLConnector();

		InputStream stream = null;
		try {
			connector.connectTagged(new URL(getUrl(false) + "bundles/dummybundle.jar"));
			fail("Expected SSLHandsakeException");
		} catch (SSLHandshakeException e) {
			// expected
		} finally {
			if (stream != null)
				IO.close(stream);
		}
	}

	public static void testConnectNoUserPass() throws Exception {
		HttpBasicAuthURLConnector connector = new HttpBasicAuthURLConnector();
		Map<String,String> config = new HashMap<String,String>();
		config.put("configs", "");
		connector.setProperties(config);

		try {
			connector.connect(new URL(getUrl(true) + "securebundles/dummybundle.jar"));
			fail("Should have thrown IOException due to missing auth");
		} catch (IOException e) {
			// expected
			assertTrue(e.getMessage().startsWith("Server returned HTTP response code: 401"));
		}
	}

	public static void testConnectWithUserPass() throws Exception {
		HttpBasicAuthURLConnector connector = new HttpBasicAuthURLConnector();
		Map<String,String> config = new HashMap<String,String>();
		config.put("configs", "testdata/http_auth.properties");
		connector.setProperties(config);

		InputStream stream = connector.connect(new URL(getUrl(true) + "securebundles/dummybundle.jar"));
		assertNotNull(stream);
		stream.close();
	}

	public static void testConnectHTTPSBadCertificate() throws Exception {
		HttpBasicAuthURLConnector connector = new HttpBasicAuthURLConnector();
		Map<String,String> config = new HashMap<String,String>();
		config.put("configs", "testdata/http_auth.properties");
		connector.setProperties(config);

		try {
			connector.connect(new URL(getUrl(false) + "securebundles/dummybundle.jar"));
			fail("Should have thrown error: invalid server certificate");
		} catch (IOException e) {
			// expected
			assertTrue(e instanceof SSLHandshakeException);
		}
	}

	public static void testConnectWithUserPassHTTPS() throws Exception {
		HttpBasicAuthURLConnector connector = new HttpBasicAuthURLConnector();
		Map<String,String> config = new HashMap<String,String>();
		config.put("configs", "testdata/http_auth.properties");
		config.put(HttpsUtil.PROP_DISABLE_SERVER_CERT_VERIFY, "true");
		connector.setProperties(config);

		InputStream stream = connector.connect(new URL(getUrl(false) + "securebundles/dummybundle.jar"));
		assertNotNull(stream);
		stream.close();
	}

	public static void testConnectWithWrongUserPass() throws Exception {
		HttpBasicAuthURLConnector connector = new HttpBasicAuthURLConnector();
		Map<String,String> config = new HashMap<String,String>();
		config.put("configs", "testdata/http_auth_wrong.properties");
		connector.setProperties(config);

		try {
			connector.connect(new URL(getUrl(true) + "securebundles/dummybundle.jar"));
			fail("Should have thrown IOException due to incorrect auth");
		} catch (IOException e) {
			// expected
			assertTrue(e.getMessage().startsWith("Server returned HTTP response code: 401"));
		}
	}

	public static void testConnectWithWrongUserPassHTTPS() throws Exception {
		HttpBasicAuthURLConnector connector = new HttpBasicAuthURLConnector();
		Map<String,String> config = new HashMap<String,String>();
		config.put("configs", "testdata/http_auth_wrong.properties");
		config.put(HttpsUtil.PROP_DISABLE_SERVER_CERT_VERIFY, "true");
		connector.setProperties(config);

		try {
			connector.connect(new URL(getUrl(false) + "securebundles/dummybundle.jar"));
			fail("Should have thrown IOException due to incorrect auth");
		} catch (IOException e) {
			// expected
			assertTrue(e.getMessage().startsWith("Server returned HTTP response code: 401"));
		}
	}

	public static void testConnectWithUserPassAndTag() throws Exception {
		HttpBasicAuthURLConnector connector = new HttpBasicAuthURLConnector();
		Map<String,String> config = new HashMap<String,String>();
		config.put("configs", "testdata/http_auth.properties");
		connector.setProperties(config);

		TaggedData data = connector.connectTagged(new URL(getUrl(true) + "securebundles/dummybundle.jar"),
				EXPECTED_ETAG);
		assertNull("Data should be null because resource not modified", data);
	}

}
