package gov.va.med.esr.service;

import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;

import javax.security.auth.login.AccountExpiredException;
import javax.security.auth.login.CredentialExpiredException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;

import gov.va.med.fw.model.EntityKey;
import gov.va.med.fw.persistent.DAOException;
import gov.va.med.fw.security.AccountDisabledException;
import gov.va.med.fw.security.AccountLockedException;
import gov.va.med.fw.security.RolePrincipal;
import gov.va.med.fw.security.UserPrincipal;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.ui.security.ChangePasswordInfo;

import gov.va.med.esr.common.model.security.ESRRolePrincipalImpl;
import gov.va.med.esr.common.model.security.ESRUserPrincipalImpl;
import gov.va.med.esr.common.util.AbstractCommonTestCase;
import gov.va.med.esr.service.impl.SecurityServiceImpl;

/**
 * Abstract test case for SecurityService. Contains most of the common methods
 * and test cases. Subclass test cases must implement authenticate method to
 * call the appropriate authenticate method.
 * 
 * @author DNS   MANSOG
 * @date Apr 27, 2005 12:11:05 PM
 */
public abstract class AbstractSecurityTestCase
    extends AbstractCommonTestCase {
    //keep the next 2 constants same as properties values in test_components.xml
    protected static int         MAX_FAILED_ATTEMPTS_TO_LOCK  = 4;
    protected static int         LOCK_TIME_ON_FAILED_ATTEMPTS = 15;
    protected static int         PASSWORD_ACTIVE_DAYS         = 65;
    protected static int         PASSWORD_EXPIRE_DAYS         = 100;
    protected static int         PASSWORD_WARNING_DAYS        = 15;
    private static final int     ACCOUNT_EXPIRE_DAYS          = PASSWORD_ACTIVE_DAYS
                                                                      + PASSWORD_EXPIRE_DAYS;
    protected static String      PASSWORD_PREFIX              = "PW_";
    protected static String      USERNAME_PREFIX              = "UN_";
    protected static String      ROLE_PREFIX                  = "ROLE_";
    private SecurityServiceImpl      securityService              = null;
    //a common user created at setup and delete at tearDown
    private ESRUserPrincipalImpl testUser;

    public SecurityServiceImpl getSecurityService() {
        return securityService;
    }

    public void setSecurityService(SecurityServiceImpl securityService) {
        this.securityService = securityService;
    }

    protected void customSetUp() throws Exception {
        super.customSetUp();
        //use the current time to generate id's.
        long id = System.currentTimeMillis();
        String userName = USERNAME_PREFIX + id;
        String password = PASSWORD_PREFIX + id;
        String roleNames = ROLE_PREFIX + id;
        testUser = addUserAndRoles(userName, password, roleNames);
    }

    /**
     * Subclasses must provide implementation of this method.
     * 
     * @param userName
     * @param password
     * @return
     * @throws LoginException
     */
    protected abstract UserPrincipal authenticate(String userName,
            String password) throws LoginException, ServiceException;

    /**
     * Test both valid and invalid authentication
     * 
     * @throws LoginException
     */
    public void testAuthentication() throws Exception {
        String userName = getTestUserName();
        String password = getTestUser().getPassword();
        validAuthenticationTest(userName, password);
        failAuthenticationTest("999" + userName, "BAD" + password);
    }

    /**
     * Test for valid authentication
     * 
     * @throws LoginException
     */
    public void testValidUserPassword() throws Exception {
        String userName = getTestUserName();
        String password = getTestUserPassword();
        validAuthenticationTest(userName, password);
    }

    /**
     * Test for failed authentication because of bad username and password.
     * 
     * @throws LoginException
     */
    public void testBadUsernamePassword() throws Exception {
        String userName = getTestUserName();
        String password = getTestUserPassword();
        failAuthenticationTest("999" + userName, "BAD" + password);
    }

    /**
     * Test for failed authentication because of bad password.
     * 
     * @throws LoginException
     */
    public void testBadPassword() throws Exception {
        String userName = getTestUserName();
        String password = getTestUserPassword();
        failAuthenticationTest(userName, "BAD" + password);
    }

    /**
     * Test for enableAccount
     * 
     * @throws ServiceException
     * @throws LoginException
     */
    public void testEnableAccount() throws ServiceException, LoginException {
        //first make sure its disabled.
        disableAccountTest(getTestUser().getEntityKey(), getTestUserName(),
                getTestUserPassword());

        //now enable the disabled account.
        enableAccountTest(getTestUser().getEntityKey(), getTestUserName(),
                getTestUserPassword());
    }

    /**
     * Test for disableAccount
     * 
     * @throws ServiceException
     * @throws LoginException
     */
    public void testDisableAccount() throws ServiceException, LoginException {
        disableAccountTest(getTestUser().getEntityKey(), getTestUserName(),
                getTestUserPassword());
    }

    /**
     * Test for unlockAccount
     * 
     * @throws ServiceException
     * @throws LoginException
     */
    public void testUnlockAccount() throws ServiceException, LoginException {
        //first make sure its locked.
        lockAccountTest(getTestUser().getEntityKey(), getTestUserName(),
                getTestUserPassword());
        //now unlock test for already locked account.
        unlockAccountTest(getTestUser().getEntityKey(), getTestUserName(),
                getTestUserPassword());
    }

    /**
     * Test for lockAccount
     * 
     * @throws ServiceException
     * @throws LoginException
     */
    public void testLockAccount() throws ServiceException, LoginException {
        lockAccountTest(getTestUser().getEntityKey(), getTestUserName(),
                getTestUserPassword());
    }

    /**
     * Test for lock account after repeated failure attempts.
     * 
     * @throws ServiceException
     * @throws LoginException
     */
    public void testRepeatedFailLoginLockAccount() throws ServiceException,
            LoginException {
        for (int i = 0; i < MAX_FAILED_ATTEMPTS_TO_LOCK - 1; i++) {
            try {
                authenticate(getTestUserName(), "dummypassword");
            } catch (FailedLoginException e) {
                //failed because of bad password.
            }
        }
        //This attempt with invalid credentials should now lock the user.
        try {
            authenticate(getTestUserName(), "dummypassword");
            fail("Account should have been locked by now.");
        } catch (AccountLockedException e) {
            //thats what we are expecting.
        }
        //Try with correct password now.
        try {
            authenticate(getTestUserName(), getTestUserPassword());
            fail("Account should have been locked by now.");
        } catch (AccountLockedException e) {
            //thats what we are expecting.
        }
    }

    /**
     * Test for un-expired password
     * 
     * @throws DAOException
     * @throws LoginException
     */
    public void testFailPasswordExpiry() throws Exception {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, -(PASSWORD_ACTIVE_DAYS - 1));
        getTestUser().setPasswordChangeDate(calendar.getTime());
        getDAO().saveObject(getTestUser());

        try {
            authenticate(getTestUserName(), getTestUserPassword());
        } catch (CredentialExpiredException e) {
            fail("Password expiry date is in future. Should not expire.", e);
        }
    }

    /**
     * Test for expired password.
     * 
     * @throws DAOException
     * @throws LoginException
     */
    public void testPasswordExpiry() throws Exception {
        expirePassword(getTestUser());
        try {
            authenticate(getTestUserName(), getTestUserPassword());
            fail("Should have thrown exception CredentialExpiredException");
        } catch (CredentialExpiredException e) {
        }
    }

    /**
     * Test for un-expired account.
     * 
     * @throws DAOException
     * @throws LoginException
     */
    public void testFailAccountExpiry() throws Exception {
        expireAccount(getTestUser());
        getTestUser().setPasswordChangeDate(new Date());
        try {
            authenticate(getTestUserName(), getTestUserPassword());
        } catch (AccountExpiredException e) {
            fail("Should not have expired. " + e.toString());
        }
    }

    /**
     * Test for expiredAccount
     * 
     * @throws DAOException
     * @throws LoginException
     */
    public void testAccountExpiry() throws Exception {
        expireAccount(getTestUser());

        try {
            authenticate(getTestUserName(), getTestUserPassword());
            fail("Should have AccountExpiredException.");
        } catch (AccountExpiredException e) {
        }
    }

    /**
     * Helper method to perform valid authentication test.
     * 
     * @param name
     * @param password
     * @throws LoginException
     */
    protected void validAuthenticationTest(String name, String password)
            throws Exception {
        UserPrincipal user = authenticate(name, password);
        assertEquals(user.getName(), name);
        assertEquals(user.getPassword(), password);
        Collection userRoles = user.getUserRoles();
        assertNotNull("Null user roles.", userRoles);
        for (Iterator iter = userRoles.iterator(); iter.hasNext();) {
            RolePrincipal rolePrincipal = (RolePrincipal) iter.next();
            assertNotNull("Null UserRole found", rolePrincipal);
        }
    }

    /**
     * Helper method for failed authentication test.
     * 
     * @param userName
     * @param password
     * @throws LoginException
     */
    protected void failAuthenticationTest(String userName, String password)
            throws Exception {
        try {
            authenticate(userName, password);
            fail("Authentication should had been failed because of wrong username/password combination.");
        } catch (FailedLoginException e) {
            //FailedLoginException means test pass
        }
    }

    /**
     * Helper method to add user and roles.
     * 
     * @param userName
     * @param password
     * @param roleName
     * @return
     * @throws DAOException
     */
    private ESRUserPrincipalImpl addUserAndRoles(String userName,
            String password, String roleName) throws DAOException {
        ESRUserPrincipalImpl userPrincipal = new ESRUserPrincipalImpl(userName);
        userPrincipal.setPassword(password);
        userPrincipal.setPasswordChangeDate(new Date());

        if (roleName != null) {
            userPrincipal.addUserRole(getRolePrincipal(roleName));
        }
        getDAO().saveObject(userPrincipal);
        return userPrincipal;
    }

    /**
     * Create a ESRRolePrincipalImpl Object given role name.
     * 
     * @param roleName
     * @return
     */
    private ESRRolePrincipalImpl getRolePrincipal(String roleName) {
        ESRRolePrincipalImpl rolePrincipal = new ESRRolePrincipalImpl();
        rolePrincipal.setName(roleName);
        return rolePrincipal;
    }

    /**
     * Helper method to test for disable account.
     * 
     * @param identifier
     * @param username
     * @param password
     * @throws LoginException
     * @throws ServiceException
     */
    protected void disableAccountTest(EntityKey identifier, String username,
            String password) throws LoginException, ServiceException {
        getSecurityService().disableAccount(identifier);
        try {
            authenticate(username, password);
            fail("Should have AccountDisabledException.");
        } catch (AccountDisabledException e) {
        }
    }

    /**
     * Helper method to test for enable account.
     * 
     * @param identifier
     * @param username
     * @param password
     * @throws LoginException
     * @throws ServiceException
     */
    protected void enableAccountTest(EntityKey identifier, String username,
            String password) throws LoginException, ServiceException {
        getSecurityService().enableAccount(identifier);
        try {
            authenticate(username, password);
        } catch (AccountDisabledException e) {
            fail("Account is enabled. Should not had thrown AccountDisabledException.");
        }
    }

    /**
     * Helper method to test for lock account.
     * 
     * @param identifier
     * @param username
     * @param password
     * @throws LoginException
     * @throws ServiceException
     */
    protected void lockAccountTest(EntityKey identifier, String username,
            String password) throws LoginException, ServiceException {
        getSecurityService().lockAccount(identifier);
        try {
            authenticate(username, password);
            fail("Should have AccountLockedException.");
        } catch (AccountLockedException e) {
        }
    }

    /**
     * Helper method to test for unlock account
     * 
     * @param identifier
     * @param username
     * @param password
     * @throws LoginException
     * @throws ServiceException
     */
    protected void unlockAccountTest(EntityKey identifier, String username,
            String password) throws LoginException, ServiceException {
        getSecurityService().unlockAccount(identifier);
        try {
            authenticate(username, password);
        } catch (AccountLockedException e) {
            fail("Account is unlocked. Should not had thrown AccountLockedException.");
        }
    }

    /**
     * Helper method to expire account.
     * 
     * @param user
     * @throws DAOException
     */
    protected void expireAccount(ESRUserPrincipalImpl user) throws DAOException {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, -(ACCOUNT_EXPIRE_DAYS));
        user.setPasswordChangeDate(calendar.getTime());
        getDAO().saveObject(user);
    }

    /**
     * Helper method to expire a password.
     * 
     * @param user
     * @throws DAOException
     */
    protected void expirePassword(ESRUserPrincipalImpl user)
            throws DAOException {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, -(PASSWORD_ACTIVE_DAYS));
        user.setPasswordChangeDate(calendar.getTime());
        getDAO().saveObject(user);
    }

    public void testChangePassword() throws LoginException, ServiceException {
        String changedPassword = "new";
        changePassword(getTestUserName(), getTestUserPassword(),
                changedPassword);
        authenticate(getTestUserName(), changedPassword);
    }

    public void testPasswordWarningDays() throws DAOException, LoginException,
            ServiceException {
        ESRUserPrincipalImpl user = getTestUser();
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, -(PASSWORD_ACTIVE_DAYS
                - PASSWORD_WARNING_DAYS + 1));
        user.setPasswordChangeDate(calendar.getTime());
        getDAO().saveObject(user);

        ESRUserPrincipalImpl newUser = (ESRUserPrincipalImpl) authenticate(
                user.getName(), user.getPassword());
        assertTrue("Should be in a warning days.",
                newUser.isPasswordChangeWarningDays());
    }

    public void testChangePasswordOnExpiredPassword() throws LoginException,
            ServiceException, DAOException {
        expirePassword(getTestUser());
        String newPassword = "new";
        changePassword(getTestUserName(), getTestUserPassword(), newPassword);
        authenticate(getTestUserName(), newPassword);
    }

    public void testChangePasswordOnExpiredAccount() throws LoginException,
            ServiceException, DAOException {
        expireAccount(getTestUser());
        String newPassword = "new";
        try {
            changePassword(getTestUserName(), getTestUserPassword(),
                    newPassword);
            fail("Account should be expired by now. Should not allow change password on expired account.");
        } catch (AccountExpiredException ex) {
            //as expected.
        }
    }

    private void changePassword(String username, String password,
            String newPassword) throws LoginException, ServiceException {
        ChangePasswordInfo cpInfo = new ChangePasswordInfo();
        cpInfo.setUserID(username);
        cpInfo.setPassword(password);
        cpInfo.setNewPassword(newPassword);
        getSecurityService().changePassword(cpInfo);
    }

    /**
     * Helper method to delete user
     * 
     * @param userPrincipal
     * @throws DAOException
     */
    protected void deleteUser(UserPrincipal userPrincipal) throws DAOException {
        getDAO().removeObject(userPrincipal.getEntityKey());
    }

    public ESRUserPrincipalImpl getTestUser() {
        return testUser;
    }

    public String getTestUserName() {
        return getTestUser().getName();
    }

    public String getTestUserPassword() {
        return getTestUser().getPassword();
    }
}