/*
 * Copyright (c) 2011-2012 ICM Uniwersytet Warszawski All rights reserved.
 * See LICENCE.txt file for licensing information.
 */
package eu.emi.security.authn.x509.impl;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.security.cert.CRL;
import java.security.cert.CertStoreSpi;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLSelector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Timer;

import static org.junit.Assert.*;

import org.apache.commons.io.FileUtils;
import org.bouncycastle.util.encoders.Base64;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import eu.emi.security.authn.x509.RiskyIntegrationTests;
import eu.emi.security.authn.x509.StoreUpdateListener;
import eu.emi.security.authn.x509.helpers.ObserversHandler;
import eu.emi.security.authn.x509.helpers.crl.OpensslCRLStoreSpi;
import eu.emi.security.authn.x509.helpers.crl.PlainCRLStoreSpi;
import eu.emi.security.authn.x509.helpers.trust.OpensslTruststoreHelper;

public class CRLTest
{
	static
	{
		//Required as we call low-level code directly (OpensslCRLStoreSpi)
		CertificateUtils.configureSecProvider();
	}
	
	private int notificationOK;
	private int localPort;
	private int opensslWarn, opensslErr;
	
	
	private static File initDir() throws IOException
	{
		File dir = new File("target/test-tmp/crls/diskCache");
		FileUtils.deleteDirectory(dir);
		dir.mkdirs();
		return dir;
	}
	
	@Test
	public void testUpdateCleanup() throws Exception
	{
		File dir = initDir();
		
		Timer t = new Timer(true);
		List<String> crls = new ArrayList<String>();
		String crlURL1 = dir.getPath() + "/*.in";
		crls.add(crlURL1);
		File target = new File(dir, "file.in");
		FileUtils.copyFile(new File("src/test/resources/test-pems/crls/relaxationsubca.crl"), 
				target);
		
		CRLParameters params = new CRLParameters(crls, 250, 
				5000, dir.getPath());
		PlainCRLStoreSpi store = new PlainCRLStoreSpi(params, t, 
				new ObserversHandler());
		store.start();


		checkCRL("CN=the subca CA,OU=Relaxation,O=Utopia,L=Tropic,C=UG", store, 1);
		target.delete();
		Thread.sleep(500);
		checkCRL("CN=the subca CA,OU=Relaxation,O=Utopia,L=Tropic,C=UG", store, 0);
		
		store.dispose();
	}	
	
	
	@Test
	public void testNotificationsAndUpdate() throws Exception
	{
		File dir = initDir();
		
		Timer t = new Timer(true);
		List<String> crls = new ArrayList<String>();
		final String crlURL1 = "http://127.0.0.1/non-existing/crl.pem";
		final String crlURL2 = "http://127.0.0.1/non-existing2/crl2.pem";
		crls.add(crlURL1);
		crls.add(crlURL2);
		String base64URL = new String(Base64.encode(crlURL1.getBytes())) + "-crl.der";
		FileUtils.copyFile(new File("src/test/resources/test-pems/crls/relaxationsubca.crl"), 
				new File(dir, base64URL));
		
		CRLParameters params = new CRLParameters(crls, 500, 
				100, dir.getPath());
		notificationOK=0;
		StoreUpdateListener listener = new StoreUpdateListener()
		{
			public void loadingNotification(String crlLocation, String type, 
					Severity level,
					Exception cause)
			{
				assertEquals(type, StoreUpdateListener.CRL);
				if (level.equals(Severity.ERROR))
				{
					assertEquals(crlURL2, crlLocation);
					assertTrue(cause instanceof IOException);
					notificationOK++;
				} else if (level.equals(Severity.WARNING))
				{
					assertEquals(crlURL1, crlLocation);
					assertNotNull(cause);
					assertTrue(cause.toString(), cause instanceof IOException);
					assertTrue(cause.getMessage().contains("cached copy"));
					notificationOK++;
				}
			}
		};
		ObserversHandler observers = new ObserversHandler(Collections.singleton(listener));
		PlainCRLStoreSpi store = new PlainCRLStoreSpi(params, t, observers);
		store.start();

		assertEquals(2, notificationOK);
		observers.removeObserver(listener);
		observers.addObserver(listener);
		Thread.sleep(750);
		assertEquals(4, notificationOK);
		store.setUpdateInterval(-1);
		Thread.sleep(750);
		assertEquals(4, notificationOK);
		store.dispose();
	}
	
	@Test
	public void testTimeout() throws Exception
	{
		Thread server = new Thread()
		{
			public void run()
			{
				try
				{
					ServerSocket ss = new ServerSocket(0, 0,
							InetAddress.getByName("127.0.0.1"));
					localPort = ss.getLocalPort();
					Socket s = ss.accept();
					System.out.println("Got connection");
					Thread.sleep(5000);
					s.close();
					ss.close();
				} catch (Exception e)
				{
					fail(e.toString());
				}
			}
		};
		server.start();
		Thread.sleep(250);
		
		File dir = initDir();
		
		Timer t = new Timer(true);
		List<String> crls = new ArrayList<String>();
		final String crlURL1 = "http://127.0.0.1:"+ localPort + "/crl.pem";
		crls.add(crlURL1);
		CRLParameters params = new CRLParameters(crls, -1, 500, dir.getPath());
		notificationOK=0;
		StoreUpdateListener listener = new StoreUpdateListener()
		{
			public void loadingNotification(String crlLocation, String type, Severity level,
					Exception cause)
			{
				assertEquals(type, StoreUpdateListener.CRL);
				assertEquals(level, Severity.ERROR);
				assertEquals(crlURL1, crlLocation);
				assertTrue(cause instanceof SocketTimeoutException);
				System.out.println(crlLocation + " " + cause.toString());
				notificationOK++;
			}
		};
		long start = System.currentTimeMillis();
		PlainCRLStoreSpi store = new PlainCRLStoreSpi(params, t, new ObserversHandler(
				Collections.singleton(listener)));
		store.start();

		assertEquals(1, notificationOK);
		start = System.currentTimeMillis() - start;
		assertTrue(start < 500*3);
		store.dispose();
	}
	
	@Test
	public void testLoadPlain() throws Exception
	{
		File dir = initDir();
		
		Timer t = new Timer(true);
		List<String> crls = new ArrayList<String>();
		String crlURL1 = "http://www.man.poznan.pl/plgrid-ca/crl.pem";
		String crlURL2 = "http://127.0.0.1/non-existing/crl.pem";
		String crlURL3 = "src/test/resources/test-pems/crls/*.pem";
		crls.add(crlURL1);
		crls.add(crlURL2);
		crls.add(crlURL3);
		String base64URL1 = new String(Base64.encode(crlURL1.getBytes())) + "-crl.der";
		String base64URL2 = new String(Base64.encode(crlURL2.getBytes())) + "-crl.der";
		FileUtils.copyFile(new File("src/test/resources/test-pems/crls/relaxationsubca.crl"), 
				new File(dir, base64URL2));
		
		CRLParameters params = new CRLParameters(crls, -1, 
				5000, dir.getPath());
		PlainCRLStoreSpi store = new PlainCRLStoreSpi(params, t, new ObserversHandler());
		store.start();
		
		checkCRL("CN=Polish Grid CA,O=GRID,C=PL", store, 1);
		String[] ls = dir.list();
		assertTrue(ls.length == 2);
		assertTrue(ls[0].equals(base64URL1) || ls[1].equals(base64URL1));

		checkCRL("CN=the subca CA,OU=Relaxation,O=Utopia,L=Tropic,C=UG", store, 1);
		
		checkCRL("CN=the trusted CA,OU=Relaxation,O=Utopia,L=Tropic,C=UG", store, 1);
		
		checkCRL("CN=missing CA,C=UG", store, 0);
		
		assertEquals(crls, store.getLocations());
		store.dispose();
	}

	@Test
	@Category(RiskyIntegrationTests.class)
	public void testMemoryFootprint() throws Exception
	{
		File dir = new File("target/test-tmp/crls/copiedCrls");
		FileUtils.deleteDirectory(dir);
		dir.mkdirs();

		int N = 100;
		File crl1 = new File("src/test/resources/test-pems/crls/relaxationsubca.crl");
		File crl2 = new File("src/test/resources/test-pems/crls/tropiccacrl.pem");
		for (int i=0; i<N; i++)
		{
			FileUtils.copyFile(crl1, new File(dir, "crl1_"+i+".pem"));
			FileUtils.copyFile(crl2, new File(dir, "crl2_"+i+".pem"));
		}
		
		Timer t = new Timer(true);
		List<String> crls = new ArrayList<String>();
		crls.add(dir.getPath()+"/*.pem");
		
		CRLParameters params = new CRLParameters(crls, -1, 
				5000, dir.getPath());
		
		int M = 150;
		PlainCRLStoreSpi[] stores = new PlainCRLStoreSpi[M];
		for (int i=0; i<M; i++)
		{
			stores[i] = new PlainCRLStoreSpi(params, t, new ObserversHandler());
			stores[i].start();
			if ((i %10) == 0)
			{
				Runtime r = Runtime.getRuntime();
				System.out.println("Loaded " + i + "\t: " + ((r.totalMemory()-r.freeMemory()))/1024 + "kb");
			}
		}
		
		for (int i=0; i<M; i++)
		{
			checkCRL("CN=the subca CA,OU=Relaxation,O=Utopia,L=Tropic,C=UG", stores[i], N);
		
			checkCRL("CN=the trusted CA,OU=Relaxation,O=Utopia,L=Tropic,C=UG", stores[i], N);
		
			assertEquals(crls, stores[i].getLocations());
		}
		
		for (int i=0; i<M; i++)
			stores[i].dispose();
	}

	
	
	@Test
	public void testLoadOpenssl() throws Exception
	{
		Timer t = new Timer(true);
		opensslErr = 0;
		opensslWarn = 0;
		StoreUpdateListener listener = new StoreUpdateListener()
		{
			public void loadingNotification(String crlLocation, String type, Severity level,
					Exception cause)
			{
				assertEquals(type, StoreUpdateListener.CRL);
				if (level == Severity.ERROR)
					opensslErr++;
				else if (level == Severity.WARNING)
					opensslWarn++;
			}
		};
		
		OpensslCRLStoreSpi store = new OpensslCRLStoreSpi(
				"src/test/resources/openssl-testcrldir", -1, t, 
				new ObserversHandler(Collections.singleton(listener)), false);

		
		checkCRL("CN=the trusted CA,OU=Relaxation,O=Utopia,L=Tropic,C=UG", store, 1);
		assertEquals(1, opensslErr);
		assertEquals(0, opensslWarn);
		
		store.dispose();
	}

	@Test
	public void checkPattern() throws Exception
	{
		assertNotNull(OpensslTruststoreHelper.getFileHash("5a1a2F89.r0", 
				"^([0-9a-fA-F]{8})\\.r[\\d]+$"));
	}
	
	private static void checkCRL(String caDN, CertStoreSpi store, int expected) throws Exception
	{
		X509CRLSelector selector = new X509CRLSelector();
		selector.addIssuerName(caDN);
		Collection<? extends CRL> matched = store.engineGetCRLs(selector);
		assertEquals(expected, matched.size());
		if (expected > 0)
		{
			X509CRL crl = (X509CRL) matched.iterator().next();
			assertTrue(X500NameUtils.equal(crl.getIssuerX500Principal(), caDN));
		}
	}
}
