/**
 * This work was created by participants in the DataONE project, and is
 * jointly copyrighted by participating institutions in DataONE. For 
 * more information on DataONE, see our web site at http://dataone.org.
 *
 *   Copyright ${year}
 *
 * Licensed 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.
 * 
 * $Id$
 */

package org.dataone.client.auth;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map.Entry;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSocket;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.SystemUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.dataone.client.exception.ClientSideException;
import org.dataone.client.v1.impl.MultipartCNode;
import org.dataone.configuration.Settings;
import org.dataone.service.exceptions.BaseException;
import org.dataone.service.types.v1.Session;
import org.dataone.service.types.v1.Subject;
import org.dataone.service.types.v1.SubjectInfo;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

public class CertificateManagerTest {

    private static final String user_cert_location = Settings.getConfiguration().getString("certificate.location");
    
    private static final String CA_VALID = "cilogon-basic";
    private static final String CA_INVALID = "cilogon-silver";
    

    @Before
    public void setUp() throws Exception {
     // can override the location of the PEM cert
//        System.out.println("TEST SETUP: setting CertificateLocation to:" + user_cert_location);
//        CertificateManager.getInstance().setCertificateLocation(user_cert_location);
    }

    @Test
    public void testHarnessCheck() {
        assertTrue(true);
    }
    
    @Test
    public void showTLSProtocols() throws NoSuchAlgorithmException, KeyManagementException, IOException {
        
        Provider[] ps = Security.getProviders();
        for (Provider p : ps) {
            System.out.println(p.getName() + ": " + p.getClass().getCanonicalName());
            for (Entry<Object,Object> n : p.entrySet()) {
                if (n.getKey().toString().contains("SSLContext"))
                    System.out.println(String.format("    %s : %s", n.getKey(),
                            n.getValue()));
                            
            }
        }
        
        System.out.println("");
        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(null, null, null);
        
        javax.net.ssl.SSLSocketFactory factory = (javax.net.ssl.SSLSocketFactory)ctx.getSocketFactory();
        SSLSocket engine = (SSLSocket)factory.createSocket();
        System.out.println(engine.getClass().getCanonicalName());
        
//        SSLEngine engine = ctx.createSSLEngine();
        String[] prots = engine.getSupportedProtocols();
        System.out.println("Supported Protocols: " + prots.length);
        for (String prot : prots) {
            System.out.println(" " + prot);
        }
        prots = engine.getEnabledProtocols();

        System.out.println("Enabled Protocols: " + prots.length);
        for(String prot: prots) {
             System.out.println(" " + prot);
        }
    }
    
    @Test
    public void testTrustManager() {
    	
        // Load the manager itself
        CertificateManager cm = CertificateManager.getInstance();
        assertNotNull(cm);
        
        // Get a certificate for the Root CA
        X509Certificate caCert = cm.getCACert("cn=dataone root ca,dc=dataone,dc=org");
        assertNotNull(caCert);
        System.out.println(caCert.getSubjectDN());
        //cm.displayCertificate(caCert);	
    }
    
   // this was a temporary test to test against a real SSL handshake - if necessary
    // move it to d1_integration
//    @Test
    public void testWildcardCert() throws BaseException, IOException, ClientSideException {
    	String[] cns = {"https://cn-dev-unm-1.test.dataone.org/cn",
    			"https://cn-dev-ucsb-1.test.dataone.org/cn",
    			"https://mn-demo-5.test.dataone.org/knb/d1/mn",
    			"https://cn-dev-orc-1.test.dataone.org/cn"};
    	for (String url : cns) {
    		System.out.println(url);
    		MultipartCNode cn = new MultipartCNode("https://cn-dev-unm-1.test.dataone.org/cn");
    		try {
    			cn.ping();
    		} catch (BaseException e) {
    			// TODO:  get getLatestRequestUrl to work again
 //   			System.out.println("Failed: " + cn.getLatestRequestUrl());
    			System.out.println("Failed: " + e.getDescription());
    		}
    	}
    	
    	
    }
    
    
    @Ignore("will not pass until certificates installed on Hudson")
    @Test
    public void testCertificateManager() {
        
        // Load the manager itself
        CertificateManager cm = CertificateManager.getInstance();
        assertNotNull(cm);
        
        // Get a certificate for the Root CA
        X509Certificate caCert = cm.getCACert("cn=dataone root ca,dc=dataone,dc=org");
        assertNotNull(caCert);
        cm.displayCertificate(caCert);

        // Load the subject's certificate
        X509Certificate cert = cm.loadCertificate();
        assertNotNull(cert);
        cm.displayCertificate(cert);
        
        // Verify the subject's certificate
        boolean valid = CertificateManager.verify(cert, caCert);
        assertTrue(valid);
    }
 
    
    
    
    
    @Ignore("will not pass until certificates installed on Hudson")
    @Test
    public void testCertificateManagerExtension() {
    	try {
	        // Load the subject's certificate
	        X509Certificate cert = CertificateManager.getInstance().loadCertificate();
	        assertNotNull(cert);
			SubjectInfo subjectInfo = CertificateManager.getInstance().getSubjectInfo(cert);
			if (subjectInfo != null) {
				// the certificate should match the first person in the subjectInfo
				String serviceSubject = CertificateManager.getInstance().standardizeDN(subjectInfo.getPerson(0).getSubject().getValue());
				String certSubject = CertificateManager.getInstance().getSubjectDN(cert);
				System.out.println("Subject from certificate extension: " + serviceSubject);
				assertEquals(serviceSubject, certSubject);
			}
		} catch (Exception e) {
			e.printStackTrace();
			fail();
		} 
        
       
    }
    
    @Ignore("will not pass until certificates installed on Hudson")
    @Test
    public void testCustomCertificateManager() {
        
        // Get a certificate for the Root CA
        X509Certificate caCert = CertificateManager.getInstance().getCACert(CA_VALID);
        assertNotNull(caCert);
        CertificateManager.getInstance().displayCertificate(caCert);

        // Load the subject's certificate
        X509Certificate certificate = CertificateManager.getInstance().loadCertificate();
        assertNotNull(certificate);
        CertificateManager.getInstance().displayCertificate(certificate);
        
        // get the private key
        PrivateKey key = CertificateManager.getInstance().loadKey();
        
        // register as the subject
        String subjectDN = CertificateManager.getInstance().getSubjectDN(certificate);
        CertificateManager.getInstance().registerCertificate(subjectDN, certificate, key);
        
        // load the SSL for custom Session
        Session session = new Session();
        Subject subject = new Subject();
        subject.setValue(subjectDN);
		session.setSubject(subject );
        try {
			SSLSocketFactory ssl = CertificateManager.getInstance().getSSLSocketFactory(session.getSubject().getValue());
			assertNotNull(ssl);
		} catch (Exception e) {
			e.printStackTrace();
			fail();
		}
        
    }
    
    @Ignore("will not pass untilcertificates installed on Hudson")
    @Test
    public void testIncorrectCA() {
        
        // Load the manager itself
        CertificateManager cm = CertificateManager.getInstance();
        assertNotNull(cm);
        
        // Get a certificate for the Root CA
        X509Certificate caCert = cm.getCACert(CA_INVALID);
        assertNotNull(caCert);
        cm.displayCertificate(caCert);

        // Load the subject's certificate
        X509Certificate cert = cm.loadCertificate();
        assertNotNull(cert);
        cm.displayCertificate(cert);
        
        // Verify the subject's certificate, but expect this to fail because we
        // are using the incorrect CA certificate
        boolean valid = CertificateManager.verify(cert, caCert);
        assertFalse(valid);
    }
    
    @Test
    public void testStandardizeSubjectDN() {
    	try {
    		// different permutations on the same subject
    		String dn1 = "CN=test,DC=dataone,DC=org";
    		String dn2 = "cn=test,dc=dataone,dc=org";
    		String dn3 = "CN=test, DC=dataone, DC=org";
    		String dn4 = "DC=org, DC=dataone, CN=test";
    		
    		// d1 == d2
    		assertEquals(
    				dn1, 
    				CertificateManager.getInstance().standardizeDN(dn2));
    		// d1 == d3
    		assertEquals(
    				dn1, 
    				CertificateManager.getInstance().standardizeDN(dn3));
    		// d1 != d4
    		assertFalse(
    				CertificateManager.getInstance().standardizeDN(dn1).equals( 
    				CertificateManager.getInstance().standardizeDN(dn4)));
    		
    		// equalsDN method
    		assertTrue(
    				CertificateManager.getInstance().equalsDN(dn1, dn2));
    		
		} catch (Exception e) {
			e.printStackTrace();
			fail();
		}    
    }
    
    @Test
    public void testDecodeSubjectDN() {
    	try {
    		// different special characters encoded using UTF-7
    		String dn1 = "CN=Fl\\+AOE-via Pezzini T6821,O=Google,C=US,DC=cilogon,DC=org"; // Flávia Pezzini
    		String dn1Expected = "CN=Fl\\+AOE-via Pezzini T6821,O=Google,C=US,DC=cilogon,DC=org";
    		String dn1Incorrect = "CN=Flávia Pezzini T6821,O=Google,C=US,DC=cilogon,DC=org";

    		String dn2 = "CN=\\+aQVbUA-,O=Google,C=US,DC=cilogon,DC=org"; //椅子 -- "chair" in Chinese
    		String dn2Expected = "CN=\\+aQVbUA-,O=Google,C=US,DC=cilogon,DC=org";
    		String dn2Incorrect = "CN=椅子,O=Google,C=US,DC=cilogon,DC=org";
    		
    		// dn1 = expected
    		assertEquals(
    				dn1Expected, 
    				CertificateManager.getInstance().standardizeDN(dn1));
    		// dn2 == expected
    		assertEquals(
    				dn2Expected, 
    				CertificateManager.getInstance().standardizeDN(dn2));
    		// no decoding should be performed
    		assertFalse(
    				CertificateManager.getInstance().standardizeDN(dn1).equals( 
    				CertificateManager.getInstance().standardizeDN(dn1Incorrect)));
    		// equalsDN method
    		assertTrue(
    				CertificateManager.getInstance().equalsDN(CertificateManager.getInstance().standardizeDN(dn1), dn1Expected));
    		// equalsDN method
    		assertFalse(
    				CertificateManager.getInstance().equalsDN(dn2, dn2Incorrect));
    		
		} catch (Exception e) {
			e.printStackTrace();
			fail();
		}    
    }
    
    
    /**
     * tests that exception thrown when a bad subject value is passed in
     */
    @Test
    public void testGetSSLSocketFactory_badSubjectValue() {
    	try {
    		CertificateManager.getInstance().getSSLSocketFactory("blah_blah");
    	} catch (KeyStoreException e) {
    		// this is what we expect
    	} catch (Exception e) {
    		e.printStackTrace();
    		fail();
    	} 
    }
    
    @Test
    public void testLocateDefaultCertificate() {
    	try {
    		File f = CertificateManager.getInstance().locateDefaultCertificate();
    		System.out.println("Default Certificate Loation: " + f.getAbsolutePath());
    		assertTrue(f.exists());
    		String userTmpDir = (System.getProperty("tmpdir") == null) ? "/tmp" : System.getProperty("tmpdir");
    		System.out.println("user tempDir: " + userTmpDir);
    		assertTrue(f.getAbsolutePath().startsWith(userTmpDir + "/x509up_u"));
    		
    	} catch (FileNotFoundException e) {
    		// ok
    	}
    }
    
    @Test
    public void testTLSPreferenceSetting_TLS_Alias() {
        
        // this "TLS" property will work with java 6,7,8 runtimes; not java 5; maybe java 9
        Settings.getConfiguration().setProperty("tls.protocol.preferences", "TLS");
        try {
            CertificateManager.getInstance().getSSLSocketFactory((String)null);
        } catch (UnrecoverableKeyException | KeyManagementException
                | KeyStoreException | CertificateException
                | NoSuchAlgorithmException | IOException e) {
            e.printStackTrace();
            fail("Threw exception when 'TLS' alias provided");
        }
    }
    
    @Test
    public void testTLSPreferenceSetting_ForwardCompatible() {
        
        // this "TLS" property will work with java 6,7,8 runtimes; not java 5; maybe java 9
        Settings.getConfiguration().setProperty("tls.protocol.preferences", "TLSv1.3, TLSv1.2, TLS");
        try {
            CertificateManager.getInstance().getSSLSocketFactory((String)null);
        } catch (UnrecoverableKeyException | KeyManagementException
                | KeyStoreException | CertificateException
                | NoSuchAlgorithmException | IOException e) {
            e.printStackTrace();
            fail("Threw exception when 'TLS' alias provided");
        } finally {
            Settings.getConfiguration().setProperty("tls.protocol.preferences", CertificateManager.defaultTlsPreferences);
        }
    }
    
    @Test
    public void testTLSPreferenceSetting_NoRealProtocols() {
        
        // this "TLS" property will work with java 6,7,8 runtimes; not java 5; maybe java 9
        Settings.getConfiguration().setProperty("tls.protocol.preferences", "foo, weboiudg");
        try {
            CertificateManager.getInstance().getSSLSocketFactory((String)null);
            fail("Didn't throw exception when only fake protocols ('foov1.2, weboiudg') were provided");
        } catch (UnrecoverableKeyException | KeyManagementException
                | KeyStoreException | CertificateException
                | NoSuchAlgorithmException | IOException e) {
            e.printStackTrace();
            
        } finally {
            Settings.getConfiguration().setProperty("tls.protocol.preferences", CertificateManager.defaultTlsPreferences);
        }
    }
    
//    @Test
    public void testSetupSSLSocketFactory() throws UnrecoverableKeyException, KeyManagementException, 
    NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException {
     
        System.out.println(SystemUtils.JAVA_RUNTIME_NAME + " " + SystemUtils.JAVA_RUNTIME_VERSION);
        System.out.println("%%%%%%%%%%%%%%%%% SSLContext Profile %%%%%%%%%%%%%%%%%%%");
        Provider[] ps = Security.getProviders();
        for (Provider p : ps) {
            System.out.println(p.getName() + ": " + p.getClass().getCanonicalName());
            for (Entry<Object,Object> n : p.entrySet()) {
                if (n.getKey().toString().contains("SSLContext"))
                    System.out.println(String.format("    %s : %s", n.getKey(),
                            n.getValue()));

            }
        }

        System.out.println("");
        SSLContext ctx = SSLContext.getInstance("TLS");
        String protocol = ctx.getProtocol();
        System.out.println(protocol);
        ctx.init(null, null, null);

        javax.net.ssl.SSLSocketFactory factory = (javax.net.ssl.SSLSocketFactory)ctx.getSocketFactory();
        SSLSocket engine = (SSLSocket)factory.createSocket();
        System.out.println("Engine impl: " + engine.getClass().getCanonicalName());

        //            SSLEngine engine = ctx.createSSLEngine();
        String[] prots = engine.getSupportedProtocols();
        System.out.println("Supported Protocols: " + prots.length);
        for (String prot : prots) {
            System.out.println(" " + prot);
        }

//        engine.setEnabledProtocols(prots);
        prots = engine.getEnabledProtocols();

        System.out.println("Enabled Protocols: " + prots.length);
        for(String prot: prots) {
            System.out.println(" " + prot);
        }
        
    	SSLSocketFactory sf = CertificateManager.getInstance().getSSLSocketFactory((String)null);
    	Scheme sch = new Scheme("https", 443, sf );
    	DefaultHttpClient hc = new DefaultHttpClient();
        hc.getConnectionManager().getSchemeRegistry().register(sch);
        
//        HttpGet req = new HttpGet("https://www.howsmyssl.com/");
//        HttpResponse response = hc.execute(req);
//        System.out.println("");
//
//        InputStream is = response.getEntity().getContent();
//        File targetFile = new File("/Users/rnahf/Documents/targetFile.html");
//        FileUtils.copyInputStreamToFile(is, targetFile);
//        String s = IOUtils.toString(is, "UTF-8");
//        int beginIndex = 0;
//        for (int i = 0; i< s.length()+80; i+=80) {
//            i = i>s.length()-1 ? s.length() : i;
//            System.out.println(s.substring(beginIndex, i));
//            beginIndex = i;
//        }
    }
    
}