package eu.dnetlib.openaire.aas;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.HashMap;
import java.util.Map;

import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.opensaml.DefaultBootstrap;
import org.opensaml.lite.common.SAMLObject;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.Attribute;
import org.opensaml.saml2.core.AttributeStatement;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.schema.XSString;
import org.springframework.context.ApplicationContext;

import pl.edu.icm.yadda.aas.audit.user.AssertionSubjectUserIdExtractor;
import pl.edu.icm.yadda.aas.refresher.impl.AttributeStatementBasedAssertionRefresher;
import pl.edu.icm.yadda.service2.user.UserCatalog;
import pl.edu.icm.yadda.service2.user.credential.LoginPasswordCredential;
import pl.edu.icm.yadda.service2.user.model.GroupName;
import pl.edu.icm.yadda.service2.user.model.User;
import pl.edu.icm.yadda.service2.user.model.UserAttributesConstants;
import pl.edu.icm.yadda.spring.utils.SpringUtils;
import eu.dnetlib.enabling.aas.DNetAAError;
import eu.dnetlib.enabling.aas.DNetAuthenticateResponse;
import eu.dnetlib.enabling.aas.IAAService;
import eu.dnetlib.enabling.aas.client.AssertionRefreshingHelper;
import eu.dnetlib.enabling.aas.client.ClientSecurityServiceHelper;
import eu.dnetlib.enabling.aas.holder.IDataHolder;
import eu.dnetlib.enabling.aas.saml.adapter.AdapterFactoryRegistry;
import eu.dnetlib.enabling.aas.saml.adapter.IAdapter;
import eu.dnetlib.enabling.aas.xacml.ctx.DecisionType.DECISION;

/**
 * User authentication tests.
 * @author mhorst
 *
 */
public class UserAuthenticationTest {
//	int casualUserRefrCount = 10;
	int casualUserRefrCount = 5;
//	String casualUserValidFor = "00:10:30";
	String casualUserValidFor = "00:00:02";
	long timeWindowMillis = 2000;
	
	int sudoerRefrCount = 20;
	String sudoerValidFor = "168:00:00";
	
	UserCatalog userCatalog = null;
	IAAService aaService = null;
	IDataHolder<SAMLObject[]> sharedDataHolder = null;


	@BeforeClass
	public static void initClass() throws Exception {
//		required by openSAML
		DefaultBootstrap.bootstrap();
//		required by adapters model
		AdapterFactoryRegistry.bootstrap();
	}
	
	@SuppressWarnings("unchecked")
	@Before
	public void init() {
		userCatalog = (UserCatalog) SpringUtils.getSpringContext(
		"classpath:/eu/dnetlib/openaire/aas/ldap-context.xml").getBeansOfType(
				UserCatalog.class).values().iterator().next();
		ApplicationContext aasContext = SpringUtils.getSpringContext(
		"classpath:/eu/dnetlib/openaire/aas/aas2-client-context.xml");
		aaService = (IAAService) aasContext.getBean("client");
		sharedDataHolder = (IDataHolder<SAMLObject[]>) aasContext.getBean("SharedDataHolder");
	}
	
	@Test
	public void testAuthenticatingCasualUser() throws Exception {
		String userId = "someUser_" + System.currentTimeMillis();
		String userName = "John Smith";
		String userEmail = "some.user@host.eu";
		String password = "somepass";
		boolean wasCreated = false;
		try {
//			1) registering new casual user in LDAP
			User user = new User();
			user.setId(userId);
			Map<String, String> attributes = new HashMap<String, String>();
			attributes.put(UserAttributesConstants.EMAIL, userEmail);
			attributes.put(UserAttributesConstants.FULL_NAME, userName);
			user.setAttributes(attributes);
			userCatalog.addUser(user);
			wasCreated = true;
//			1b) setting password
			LoginPasswordCredential passCred = new LoginPasswordCredential();
			passCred.setUserId(userId);
			passCred.setPassword(password);
			userCatalog.addCredential(passCred);
			
//			2) authenticating user
			DNetAuthenticateResponse response = aaService.authenticate(
					ClientSecurityServiceHelper.buildUserAuthnRequest(
					userEmail, password, null, ClientSecurityServiceHelper.AUTHN_TYPE_USER, false));
//			validating authn results
			assertNotNull(response);
			assertTrue(response.getResult().getDecision().getDecision()==DECISION.Permit);
//			assertion containing roles is expected as a result of authentication
			org.opensaml.lite.common.SAMLObject[] receivedSAMLData = sharedDataHolder.getData();
			assertNotNull(receivedSAMLData);
			assertEquals(1, receivedSAMLData.length);
			assertTrue(receivedSAMLData[0] instanceof org.opensaml.lite.saml2.core.Assertion);
			assertTrue(receivedSAMLData[0] instanceof IAdapter<?>);
			Assertion receivedAssertion = (Assertion) ((IAdapter<?>) receivedSAMLData[0]).getAdaptedObject();
			assertNotNull(receivedAssertion);
			assertEquals(casualUserRefrCount, Integer.valueOf(getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_REFRESHES_COUNT, receivedAssertion)).intValue());
			assertNull(getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_TIME_FRAME, receivedAssertion));
			long validFor = (receivedAssertion.getConditions().getNotOnOrAfter().minus(
					receivedAssertion.getConditions().getNotBefore().getMillis()).getMillis());
			assertEquals(casualUserValidFor, getElapsedTimeHoursMinutesSecondsString(validFor));
			
		} finally {
			if (wasCreated) {
				userCatalog.deleteUser(userId, null);
			}
		}
	}
	
	@Test
	public void testAuthenticatingSudoer() throws Exception {
		String userId = "someUser_" + System.currentTimeMillis();
		String userName = "John Smith";
		String userEmail = "some.user@host.eu";
		String password = "somepass";
		boolean wasCreated = false;
		try {
//			1) registering new sudoer user in LDAP
			User user = new User();
			user.setId(userId);
			Map<String, String> attributes = new HashMap<String, String>();
			attributes.put(UserAttributesConstants.EMAIL, userEmail);
			attributes.put(UserAttributesConstants.FULL_NAME, userName);
			user.setAttributes(attributes);
			userCatalog.addUser(user);
			wasCreated = true;
//			1b) setting password
			LoginPasswordCredential passCred = new LoginPasswordCredential();
			passCred.setUserId(userId);
			passCred.setPassword(password);
			userCatalog.addCredential(passCred);
//			1c) assigning to sudoers group
			userCatalog.assignUser(userId, new GroupName(null,"sudoers"));
			
//			2) authenticating user
			DNetAuthenticateResponse response = aaService.authenticate(
					ClientSecurityServiceHelper.buildUserAuthnRequest(
					userEmail, password, null, ClientSecurityServiceHelper.AUTHN_TYPE_USER, false));
//			validating authn results
			assertNotNull(response);
			assertTrue(response.getResult().getDecision().getDecision()==DECISION.Permit);
//			assertion containing roles is expected as a result of authentication
			org.opensaml.lite.common.SAMLObject[] receivedSAMLData = sharedDataHolder.getData();
			assertNotNull(receivedSAMLData);
			assertEquals(1, receivedSAMLData.length);
			assertTrue(receivedSAMLData[0] instanceof org.opensaml.lite.saml2.core.Assertion);
			assertTrue(receivedSAMLData[0] instanceof IAdapter<?>);
			Assertion receivedAssertion = (Assertion) ((IAdapter<?>) receivedSAMLData[0]).getAdaptedObject();
			assertNotNull(receivedAssertion);
			assertEquals(sudoerRefrCount, Integer.valueOf(getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_REFRESHES_COUNT, receivedAssertion)).intValue());
			assertNull(getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_TIME_FRAME, receivedAssertion));
			long validFor = (receivedAssertion.getConditions().getNotOnOrAfter().minus(
					receivedAssertion.getConditions().getNotBefore().getMillis()).getMillis());
			assertEquals(sudoerValidFor, getElapsedTimeHoursMinutesSecondsString(validFor));
			
		} finally {
			if (wasCreated) {
				userCatalog.deleteUser(userId, null);
			}
		}
	}
	
	@Test
	public void testSudoAuthentication() throws Exception {
		String sudoId = "someSudo_" + System.currentTimeMillis();
		String sudoName = "John Smith";
		String sudoEmail = "some.sudo@host.eu";
		String sudoPass = "somesudopass";
		boolean wasSudoCreated = false;
		
		String userId = "someUser_" + System.currentTimeMillis();
		String userName = "Eddie Kowalski";
		String userEmail = "some.user@host.eu";
		boolean wasUserCreated = false;

		try {
//			1) registering new sudoer user in LDAP
			User user = new User();
			user.setId(sudoId);
			Map<String, String> attributes = new HashMap<String, String>();
			attributes.put(UserAttributesConstants.EMAIL, sudoEmail);
			attributes.put(UserAttributesConstants.FULL_NAME, sudoName);
			user.setAttributes(attributes);
			userCatalog.addUser(user);
			wasSudoCreated = true;
//			1b) setting password
			LoginPasswordCredential passCred = new LoginPasswordCredential();
			passCred.setUserId(sudoId);
			passCred.setPassword(sudoPass);
			userCatalog.addCredential(passCred);
//			1c) assigning to sudoers group
			userCatalog.assignUser(sudoId, new GroupName(null,"sudoers"));
			
//			2) authenticating user
			DNetAuthenticateResponse response = aaService.authenticate(
					ClientSecurityServiceHelper.buildUserAuthnRequest(
					sudoEmail, sudoPass, null, ClientSecurityServiceHelper.AUTHN_TYPE_USER, false));
//			validating authn results
			assertNotNull(response);
			assertTrue(response.getResult().getDecision().getDecision()==DECISION.Permit);
//			assertion containing roles is expected as a result of authentication
			org.opensaml.lite.common.SAMLObject[] receivedSAMLData = sharedDataHolder.getData();
			assertNotNull(receivedSAMLData);
			assertEquals(1, receivedSAMLData.length);
			assertTrue(receivedSAMLData[0] instanceof org.opensaml.lite.saml2.core.Assertion);
			assertTrue(receivedSAMLData[0] instanceof IAdapter<?>);
			Assertion receivedAssertion = (Assertion) ((IAdapter<?>) receivedSAMLData[0]).getAdaptedObject();
			assertNotNull(receivedAssertion);
			
//			3) registering casual user
			user = new User();
			user.setId(userId);
			attributes = new HashMap<String, String>();
			attributes.put(UserAttributesConstants.EMAIL, userEmail);
			attributes.put(UserAttributesConstants.FULL_NAME, userName);
			user.setAttributes(attributes);
			userCatalog.addUser(user);
			wasUserCreated = true;
			
//			4) performing sudo authentication and checking if received user assertion
			sharedDataHolder.storeData(receivedSAMLData);
			response = aaService.authenticate(
			ClientSecurityServiceHelper.buildSudoAuthnRequest(userEmail, null));
//			validating authn results
			assertNotNull(response);
			assertTrue(response.getResult().getDecision().getDecision()==DECISION.Permit);
//			assertion containing roles is expected as a result of authentication
			receivedSAMLData = sharedDataHolder.getData();
			assertNotNull(receivedSAMLData);
			assertEquals(1, receivedSAMLData.length);
			assertTrue(receivedSAMLData[0] instanceof org.opensaml.lite.saml2.core.Assertion);
			assertTrue(receivedSAMLData[0] instanceof IAdapter<?>);
			receivedAssertion = (Assertion) ((IAdapter<?>) receivedSAMLData[0]).getAdaptedObject();
			assertNotNull(receivedAssertion);
			assertEquals(casualUserRefrCount, Integer.valueOf(getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_REFRESHES_COUNT, receivedAssertion)).intValue());
			assertNull(getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_TIME_FRAME, receivedAssertion));
			long validFor = (receivedAssertion.getConditions().getNotOnOrAfter().minus(
					receivedAssertion.getConditions().getNotBefore().getMillis()).getMillis());
			assertEquals(casualUserValidFor, getElapsedTimeHoursMinutesSecondsString(validFor));
			
		} finally {
			try {
				if (wasSudoCreated) {
					userCatalog.deleteUser(sudoId, null);
				}	
			} catch(Exception e) {
				e.printStackTrace();
			}
			if (wasUserCreated) {
				userCatalog.deleteUser(userId, null);
			}
		}
	}
	
	@Test
	public void testRefresh() throws Exception {
//		user needs to have attribute set to rather small value e.g. 
//		yadda:security:ttl=PT2S, yadda:security:refreshes-count=5
//		or system/servlet-ctx properties set:
//		aas.assertion.default.ttl=PT2S, aas.assertion.default.refreshes-count=5
		
		String userId = "someUser_" + System.currentTimeMillis();
		String userName = "John Smith";
		String userEmail = "some.user@host.eu";
		String password = "somepass";
		boolean wasCreated = false;
		try {
//			1) registering new casual user in LDAP
			User user = new User();
			user.setId(userId);
			Map<String, String> attributes = new HashMap<String, String>();
			attributes.put(UserAttributesConstants.EMAIL, userEmail);
			attributes.put(UserAttributesConstants.FULL_NAME, userName);
			user.setAttributes(attributes);
			userCatalog.addUser(user);
			wasCreated = true;
//			1b) setting password
			LoginPasswordCredential passCred = new LoginPasswordCredential();
			passCred.setUserId(userId);
			passCred.setPassword(password);
			userCatalog.addCredential(passCred);
			
//			2) authenticating user
			DNetAuthenticateResponse response = aaService.authenticate(
					ClientSecurityServiceHelper.buildUserAuthnRequest(
					userEmail, password, null, ClientSecurityServiceHelper.AUTHN_TYPE_USER, false));
//			validating authn results
			assertNotNull(response);
			assertTrue(response.getResult().getDecision().getDecision()==DECISION.Permit);
//			assertion containing roles is expected as a result of authentication
			org.opensaml.lite.common.SAMLObject[] receivedSAMLData = sharedDataHolder.getData();
			assertNotNull(receivedSAMLData);
			assertEquals(1, receivedSAMLData.length);
			assertTrue(receivedSAMLData[0] instanceof org.opensaml.lite.saml2.core.Assertion);
			assertTrue(receivedSAMLData[0] instanceof IAdapter<?>);
			Assertion receivedAssertion = (Assertion) ((IAdapter<?>) receivedSAMLData[0]).getAdaptedObject();
			assertNotNull(receivedAssertion);
			assertEquals(casualUserRefrCount, Integer.valueOf(getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_REFRESHES_COUNT, receivedAssertion)).intValue());
			assertNull(getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_TIME_FRAME, receivedAssertion));
			long validFor = (receivedAssertion.getConditions().getNotOnOrAfter().minus(
					receivedAssertion.getConditions().getNotBefore().getMillis()).getMillis());
			assertEquals(casualUserValidFor, getElapsedTimeHoursMinutesSecondsString(validFor));
			
//			wait single time-window to pass
			Thread.sleep(timeWindowMillis);
//			performing refresh
			sharedDataHolder.storeData(receivedSAMLData);
			response = aaService.authenticate(
					AssertionRefreshingHelper.buildAssertionRefreshingRequest(
							new AssertionSubjectUserIdExtractor().extractId(receivedSAMLData[0]), 
							AssertionRefreshingHelper.extractSubjectValue((org.opensaml.lite.saml2.core.Assertion) receivedSAMLData[0])));
//			validating authn results
			assertNotNull(response);
			assertTrue(response.getResult().getDecision().getDecision()==DECISION.Permit);
//			assertion containing roles is expected as a result of authentication
			receivedSAMLData = sharedDataHolder.getData();
			assertNotNull(receivedSAMLData);
			assertEquals(1, receivedSAMLData.length);
			assertTrue(receivedSAMLData[0] instanceof org.opensaml.lite.saml2.core.Assertion);
			assertTrue(receivedSAMLData[0] instanceof IAdapter<?>);
			receivedAssertion = (Assertion) ((IAdapter<?>) receivedSAMLData[0]).getAdaptedObject();
			assertNotNull(receivedAssertion);
			int lastRefrCount;
			assertTrue(casualUserRefrCount > (lastRefrCount = Integer.valueOf(getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_REFRESHES_COUNT, receivedAssertion)).intValue()));
			String timeFrame;
			assertNotNull(timeFrame = getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_TIME_FRAME, receivedAssertion));
			System.out.println("time frame: " + timeFrame);
			assertEquals(ClientSecurityServiceHelper.AUTHN_TYPE_USER, getAttributeValue(
					"yadda:authn:subject:id", receivedAssertion));
			
//			refreshing refreshed
			Thread.sleep(timeWindowMillis);
//			performing refresh
			sharedDataHolder.storeData(receivedSAMLData);
			response = aaService.authenticate(
					AssertionRefreshingHelper.buildAssertionRefreshingRequest(
							new AssertionSubjectUserIdExtractor().extractId(receivedSAMLData[0]), 
							AssertionRefreshingHelper.extractSubjectValue((org.opensaml.lite.saml2.core.Assertion) receivedSAMLData[0])));
//			validating authn results
			assertNotNull(response);
			assertTrue(response.getResult().getDecision().getDecision()==DECISION.Permit);
//			assertion containing roles is expected as a result of authentication
			receivedSAMLData = sharedDataHolder.getData();
			assertNotNull(receivedSAMLData);
			assertEquals(1, receivedSAMLData.length);
			assertTrue(receivedSAMLData[0] instanceof org.opensaml.lite.saml2.core.Assertion);
			assertTrue(receivedSAMLData[0] instanceof IAdapter<?>);
			receivedAssertion = (Assertion) ((IAdapter<?>) receivedSAMLData[0]).getAdaptedObject();
			assertNotNull(receivedAssertion);

			assertTrue(lastRefrCount > Integer.valueOf(getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_REFRESHES_COUNT, receivedAssertion)).intValue());
			assertEquals(timeFrame, getAttributeValue(
					AttributeStatementBasedAssertionRefresher.ATTRIBUTE_NAME_TIME_FRAME, receivedAssertion));
			assertEquals(ClientSecurityServiceHelper.AUTHN_TYPE_USER, getAttributeValue(
					"yadda:authn:subject:id", receivedAssertion));
			
//			wait for permanent expiration
			Thread.sleep(casualUserRefrCount*timeWindowMillis);
			sharedDataHolder.storeData(receivedSAMLData);
//			inDataHolder.storeData(null);
			response = aaService.authenticate(
					AssertionRefreshingHelper.buildAssertionRefreshingRequest(
							new AssertionSubjectUserIdExtractor().extractId(receivedSAMLData[0]), 
							AssertionRefreshingHelper.extractSubjectValue((org.opensaml.lite.saml2.core.Assertion) receivedSAMLData[0])));
			assertTrue(sharedDataHolder.getData()==null || sharedDataHolder.getData().length==0);
			assertNotNull(response);
			assertEquals(1, response.getErrors().length);
			assertEquals(DNetAAError.WARN_ASSERTION_PERM_EXPIRED, 
					response.getErrors()[0].getErrorId());

		} finally {
			if (wasCreated) {
				userCatalog.deleteUser(userId, null);
			}
		}
	}
	
	public String getElapsedTimeHoursMinutesSecondsString(long elapsedTime) {       
	    String format = String.format("%%0%dd", 2);  
	    elapsedTime = elapsedTime / 1000;  
	    String seconds = String.format(format, elapsedTime % 60);  
	    String minutes = String.format(format, (elapsedTime % 3600) / 60);  
	    String hours = String.format(format, elapsedTime / 3600);  
	    String time =  hours + ":" + minutes + ":" + seconds;  
	    return time;  
	}  
	
	String getAttributeValue(String attrName, Assertion assertion) {
		if (assertion.getAttributeStatements()!=null) {
			for (AttributeStatement attrStatement : assertion.getAttributeStatements()) {
				if (attrStatement.getAttributes()!=null) {
					for (Attribute attr : attrStatement.getAttributes()) {
						if (attrName.equals(attr.getName())) {
							XMLObject candida = attr.getAttributeValues().iterator().next();
							if (candida instanceof XSString) {
								return ((XSString)candida).getValue();
							} else {
								throw new RuntimeException("unable to retrieve " +
										attrName + " attribute value: " +
										"expected XSString instance, got: " + 
										candida.getClass().getName());
							}
						}
					}
				}
			}
		}
		return null;
	}
	
}
