/*
 * Copyright (C) 2005 - 2011 Jaspersoft Corporation. All rights reserved.
 * http://www.jaspersoft.com.
 *
 * Unless you have purchased  a commercial license agreement from Jaspersoft,
 * the following license terms  apply:
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License  as
 * published by the Free Software Foundation, either version 3 of  the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero  General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public  License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package com.jaspersoft.jasperserver.util.test;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.Date;
import java.util.HashMap;
import java.util.Properties;

import net.sf.jasperreports.engine.JRParameter;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.Authentication;
import org.springframework.security.acl.basic.SimpleAclEntry;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.TestingAuthenticationToken;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.test.AbstractDependencyInjectionSpringContextTests;

import com.jaspersoft.jasperserver.api.common.domain.ExecutionContext;
import com.jaspersoft.jasperserver.api.common.domain.impl.ExecutionContextImpl;
import com.jaspersoft.jasperserver.api.engine.common.service.EngineService;
import com.jaspersoft.jasperserver.api.engine.jasperreports.service.impl.JdbcReportDataSourceServiceFactory;
import com.jaspersoft.jasperserver.api.engine.jasperreports.service.impl.JndiJdbcReportDataSourceServiceFactory;
import com.jaspersoft.jasperserver.api.engine.scheduling.service.ReportSchedulingService;
import com.jaspersoft.jasperserver.api.metadata.common.domain.FileResource;
import com.jaspersoft.jasperserver.api.metadata.common.domain.Folder;
import com.jaspersoft.jasperserver.api.metadata.common.domain.InternalURI;
import com.jaspersoft.jasperserver.api.metadata.common.domain.client.FolderImpl;
import com.jaspersoft.jasperserver.api.metadata.common.service.RepositoryService;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.domain.JdbcReportDataSource;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.domain.JndiJdbcReportDataSource;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.domain.ReportDataSource;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.domain.client.JdbcReportDataSourceImpl;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.service.ReportDataSourceService;
import com.jaspersoft.jasperserver.api.metadata.olap.service.OlapConnectionService;
import com.jaspersoft.jasperserver.api.metadata.user.domain.ObjectPermission;
import com.jaspersoft.jasperserver.api.metadata.user.domain.ProfileAttribute;
import com.jaspersoft.jasperserver.api.metadata.user.domain.Role;
import com.jaspersoft.jasperserver.api.metadata.user.domain.Tenant;
import com.jaspersoft.jasperserver.api.metadata.user.domain.User;
import com.jaspersoft.jasperserver.api.metadata.user.domain.client.TenantImpl;
import com.jaspersoft.jasperserver.api.metadata.user.service.ObjectPermissionService;
import com.jaspersoft.jasperserver.api.metadata.user.service.ProfileAttributeService;
import com.jaspersoft.jasperserver.api.metadata.user.service.TenantService;
import com.jaspersoft.jasperserver.api.metadata.user.service.UserAuthorityService;
import com.jaspersoft.jasperserver.api.metadata.user.service.impl.ObjectPermissionServiceImpl;

/**
 * @author asokolnikov
 */
public abstract class BaseJasperServerTest extends AbstractDependencyInjectionSpringContextTests {

    public static String adminUserName = "admin";
	public static String userRoleName = "ROLE_USER";
	public static String administratorRoleName = "ROLE_ADMINISTRATOR";
	protected ExecutionContext exContext = new ExecutionContextImpl();
    private Properties jdbcProps;
    private JdbcReportDataSourceServiceFactory jdbcDataSourceServiceFactory;
    private JndiJdbcReportDataSourceServiceFactory jndiJdbcDataSourceServiceFactory;
    private RepositoryService repositoryService;
    private RepositoryService unsecureRepositoryService;
    private UserAuthorityService userAuthorityService;
    private ProfileAttributeService profileAttributeService;
	private ObjectPermissionService objectPermissionService;
	private ReportSchedulingService reportSchedulingService;
	private OlapConnectionService olapConnectionService;
    private EngineService engineService;
	protected final Log logger = LogFactory.getLog(getClass());
	private TenantService tenantService;

	protected static final String USER_JASPERADMIN = "jasperadmin";
	protected static final String ROLE_ADMINISTRATOR = "ROLE_ADMINISTRATOR";
	
    public BaseJasperServerTest() {
        super();
        setAutowireMode(AUTOWIRE_BY_NAME);
    }

    public BaseJasperServerTest(String name) {
        super(name);
        setAutowireMode(AUTOWIRE_BY_NAME);
    }


    protected String[] getConfigLocations() {
		return new String[]{"applicationContext*.xml"};
    }


    public String getNameFromURI(String uri) {
        String[] pathParts = uri.split("/");
        return pathParts[pathParts.length - 1];
    }

    protected FileResource loadAndPrepareFileResource(String resourcePath, String uri, String fileType) throws Exception {
        String resultString = loadFile(resourcePath);

        String name = getNameFromURI(uri);
        return prepareFile(name, name, name, resultString, fileType);
    }

    protected String loadFile(String fileName) throws IOException {
        InputStream is = getClass().getResourceAsStream(fileName);
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        StringBuffer sb = new StringBuffer();
        char[] cbuf = new char[1024];
        int k = 0;
        while ( (k = br.read(cbuf)) !=  -1 ) {
            sb.append(cbuf, 0, k);
        }
        br.close();
        return sb.toString();
    }

    protected FileResource prepareFile(String name, String label, String description, String content, String fileType) {

        FileResource schemaRes = (FileResource) getRepositoryService().newResource(exContext, FileResource.class);

        schemaRes.setFileType(fileType);
        schemaRes.setName(name);
        schemaRes.setLabel(label);
        schemaRes.setDescription(description);


        // Update/set the data
        schemaRes.setData(content.getBytes());

        return schemaRes;

    }

    protected Folder ensureParentFolderExists(String uri) {

    	String[] pathParts;
        StringBuffer folderPath = getParentURI(uri);

    	Folder got = getRepositoryService().getFolder(exContext, folderPath.toString());

    	if (got != null) {
    		logger.debug("Folder: " + folderPath.toString() + " exists");
    		return got;
    	}

    	logger.debug("Creating Folder: " + folderPath.toString());

    	pathParts = folderPath.toString().split("/");

    	folderPath = new StringBuffer();
    	Folder parentFolder = null;

    	for (int i = 0; i < pathParts.length; i++) {
    		if (pathParts[i].length() == 0) {
    			continue;
    		}

    		folderPath.append("/").append(pathParts[i]);

    		got = getRepositoryService().getFolder(exContext, folderPath.toString());

        	if (got == null) {
                got = new FolderImpl();
                got.setName(pathParts[i]);
                got.setLabel(pathParts[i]);
                got.setDescription(pathParts[i] + " description");
                got.setParentFolder(parentFolder);
                getRepositoryService().saveFolder(null, got);
        	}

        	parentFolder = got;
    	}

    	return got;
    }

    public StringBuffer getParentURI(String uri) {
        String[] pathParts = uri.split("/");

    	StringBuffer folderPath = new StringBuffer();

    	for (int i = 0; i < pathParts.length - 1; i++) {
    		if (pathParts[i].length() == 0) {
    			continue;
    		}
    		folderPath.append("/").append(pathParts[i]);
    		//log.debug("" + i + ": " + folderPath.toString());
    	}
        return folderPath;
    }


    public Properties getJdbcProps() {
        return jdbcProps;
    }
    
    public void setJdbcProps(Properties jdbcProps) {
    	this.jdbcProps = jdbcProps;
    }

    /**
     * Returns database product name to decide which schema should be used
     * (database specific schemas with upper case for Oracle and special date functions for SQLServer)
     * Suppose to use it instead of
     *      test.databaseFlavor=oracle
     *      test.foodmart.upperCaseNames=true
     * @param dsUri
     *      datasource uri
     * @return String
     *      database vendor name, has to be one of listed in applicationContext-semanticLayer.xml beanId = sqlGeneratorFactory
     */
    public String getDatabaseProductName(String dsUri) {
        ReportDataSource ds = (ReportDataSource) getRepositoryService().getResource(exContext, dsUri);
        ReportDataSourceService jdss;
        if (ds instanceof JndiJdbcReportDataSource) {
            jdss = jndiJdbcDataSourceServiceFactory.createService(ds);
        } else {
            jdss = jdbcDataSourceServiceFactory.createService(ds);
        }
        HashMap params = new HashMap();
        jdss.setReportParameterValues(params);
        try {
            Connection conn = (Connection) params.get(JRParameter.REPORT_CONNECTION);
            DatabaseMetaData metadata = conn.getMetaData();
            jdss.closeConnection();
            return metadata.getDatabaseProductName();
        } catch (SQLException ex) {
            throw new IllegalArgumentException("Cannot get database vendor name", ex);
        }
    }
    /**
     * Checks if foodmart.upperCaseNames property is set
     * @return boolean
     * @throws Exception 
     */
    public boolean useUpperCaseNames() throws Exception {
        return Boolean.parseBoolean(getJdbcProps().getProperty("foodmart.upperCaseNames"));
    }
    

    public JdbcReportDataSourceServiceFactory getJdbcDataSourceServiceFactory() {
        return jdbcDataSourceServiceFactory;
    }

    public void setJdbcDataSourceServiceFactory(JdbcReportDataSourceServiceFactory jdbcDataSourceServiceFactory) {
        this.jdbcDataSourceServiceFactory = jdbcDataSourceServiceFactory;
    }

    public JndiJdbcReportDataSourceServiceFactory getJndiJdbcDataSourceServiceFactory() {
        return jndiJdbcDataSourceServiceFactory;
    }

    public void setJndiJdbcDataSourceServiceFactory(JndiJdbcReportDataSourceServiceFactory jndiJdbcDataSourceServiceFactory) {
        this.jndiJdbcDataSourceServiceFactory = jndiJdbcDataSourceServiceFactory;
    }

	protected JdbcReportDataSource createJdbcReportDataSourceFromProperties(String prefix) throws Exception {
			    JdbcReportDataSourceImpl ds = new JdbcReportDataSourceImpl();
			    ds.setDriverClass(getJdbcProps().getProperty(prefix + ".jdbc.driverClassName"));
			    ds.setConnectionUrl(getJdbcProps().getProperty(prefix + ".jdbc.url"));
			    ds.setUsername(getJdbcProps().getProperty(prefix + ".jdbc.username"));
			    ds.setPassword(getJdbcProps().getProperty(prefix + ".jdbc.password"));
			    if (ds.getDriverClass() == null || ds.getConnectionUrl() == null || ds.getUsername() == null || ds.getPassword() == null) {
			    	throw new IllegalArgumentException("some jdbc props missing for prefix " + prefix);
			    }
			    return ds;
			}

	protected JdbcReportDataSource createAndSaveJdbcDSFromProps(String folderURI, String prefix) throws Exception {
		String dsURI = folderURI + "/" + prefix;
		
		JdbcReportDataSource ds = (JdbcReportDataSource) getRepositoryService().getResource(exContext, dsURI);
	
	    // Create it if is not there
	    if (ds == null) {
	    	ds = createJdbcReportDataSourceFromProperties(prefix);
	        ds.setName(prefix);
	        ds.setParentFolder(ensureParentFolderExists(dsURI));
		    ds.setLabel(prefix);
		    getRepositoryService().saveResource(exContext, ds);
	    }
	    return ds;
	}
	
	protected ProfileAttribute createTestAttr(Object principal, String name, String value) {
	    ProfileAttribute attr = getProfileAttributeService().newProfileAttribute( null );
	    attr.setPrincipal( principal );
	    attr.setAttrName( name );
	    attr.setAttrValue( value );
	    return attr;
	}

	protected Authentication setAuthenticatedUser(String username) {
	    UserDetails userDetails =
	        ((UserDetailsService)getUserAuthorityService()).loadUserByUsername(username);
	    Authentication aUser =
	        new TestingAuthenticationToken(userDetails,
	                       userDetails.getPassword(),
	                       userDetails.getAuthorities());
	    aUser.setAuthenticated(true);
	    SecurityContextHolder.getContext().setAuthentication(aUser);
	
	    logger.debug("Principal: " + aUser.getPrincipal());
	    return aUser;
	}

	/**
	 * Ensure that the minimal repository objects are set up before doing anything with the repository.
	 * Check if these objects are here and create them if they're not:
	 * - default tenant
	 * - root folder
	 * - ROLE_USER role
	 * - ROLE_ADMINISTRATOR role
	 * - admin user
	 * - admin perms on root folder for the admin user
	 * TODO
	 * - the admin user is "admin" which is non-standard; this could be broken out in its own method
	 * - short-circuit this so it's only done once per test run
	 * - createObjectPermission() could look for an existing one before creating
	 */
	protected void basicAuthSetup() {
		Tenant defaultTenant = tenantService.getDefaultTenant(null);
		if (defaultTenant == null) {
			createTenantForRoot();
		}
        Role userRole = getOrCreateRole(userRoleName);
        Role adminRole = getOrCreateRole(administratorRoleName);
        User adminUser = findOrCreateUser(adminUserName);
        getUserAuthorityService().addRole(null, adminUser, adminRole);

   		Folder root = getUnsecureRepositoryService().getFolder(exContext, "/");
    	if (root == null) {
    		root = new FolderImpl();
    		root.setName(Folder.SEPARATOR);
    		root.setLabel("root");
    		root.setDescription("Root of the folder hierarchy");
    		getUnsecureRepositoryService().saveFolder(null, root);
    	}

        createObjectPermission("/", adminRole, SimpleAclEntry.ADMINISTRATION);
		setAuthenticatedUser(BaseJasperServerTest.adminUserName);
	}

	protected Role getOrCreateRole(String roleName) {
	    Role r = getUserAuthorityService().getRole(null, roleName);
	    if (r == null) {
	        r = getUserAuthorityService().newRole(null);
	        r.setRoleName(roleName);
	        r.setExternallyDefined(false);
	        getUserAuthorityService().putRole(null, r);
	    }
	    return r;
	}

	protected User findOrCreateUser(String username) {
	    User workingUser = getUserAuthorityService().getUser(null, username);
	    if (workingUser == null) {
	        workingUser = getUserAuthorityService().newUser(null);
	        workingUser.setUsername(username);
	        workingUser.setPassword(username);
	        workingUser.setFullName(username + " user");
	        workingUser.setEnabled(true);
            workingUser.setPreviousPasswordChangeTime(new Date());
	
	        getUserAuthorityService().putUser(null, workingUser);
	    }
	    return workingUser;
	}
	
	private FileResource saveOrUpdateFile(String uri, String name, String label,
			String description, String content, String fileType) {
			
			    FileResource schemaRes = (FileResource) getRepositoryService().getResource(exContext, uri);
			
			    // Create it if is not there
			    if (schemaRes == null) {
			
			    	logger.debug("File: " + uri + " being created");
			
			        schemaRes = prepareFile(name, label, description, content, fileType);
			        schemaRes.setParentFolder(ensureParentFolderExists(uri));
			
			        getRepositoryService().saveResource(exContext, schemaRes);
			    }
			
			    return schemaRes;
			}

	protected FileResource loadAndSaveFileResource(String resourcePath, String uri, String fileType) throws Exception {
	    String resultString = loadFile(resourcePath);
	
	    String name = getNameFromURI(uri);
	    return saveOrUpdateFile(uri, name, name, name, resultString, fileType);
	}
	
    public RepositoryService getRepositoryService() {
        return repositoryService;
    }

    public void setRepositoryService(RepositoryService repository) {
    	repositoryService = repository;
    }

	public void setUnsecureRepositoryService(RepositoryService unsecureRepositoryService) {
		this.unsecureRepositoryService = unsecureRepositoryService;
	}

	public RepositoryService getUnsecureRepositoryService() {
		return unsecureRepositoryService;
	}

	public void setUserAuthorityService(UserAuthorityService userAuthorityService) {
		this.userAuthorityService = userAuthorityService;
	}

	public UserAuthorityService getUserAuthorityService() {
		return userAuthorityService;
	}

	public void setObjectPermissionService(ObjectPermissionService objectPermissionService) {
		this.objectPermissionService = objectPermissionService;
	}

	public ObjectPermissionService getObjectPermissionService() {
		return objectPermissionService;
	}

	public void setReportSchedulingService(ReportSchedulingService reportSchedulingService) {
		this.reportSchedulingService = reportSchedulingService;
	}

    public ProfileAttributeService getProfileAttributeService() {
		return profileAttributeService;
	}

	public ReportSchedulingService getReportSchedulingService() {
		return reportSchedulingService;
	}

	public void setOlapConnectionService(OlapConnectionService olapConnectionService) {
		this.olapConnectionService = olapConnectionService;
	}

	public OlapConnectionService getOlapConnectionService() {
		return olapConnectionService;
	}

    protected void logPermission(ObjectPermission perm) {
        if (perm == null) return;
        Object rpt = perm.getPermissionRecipient();
        String name = "?";
        if (rpt instanceof Role) {
            Role r = (Role) rpt;
            name = r.getRoleName() + (r.getTenantId() == null ? "" : "|" + r.getTenantId());
        } else if (rpt instanceof User) {
            User u = (User) rpt;
            name = u.getUsername() + (u.getTenantId() == null ? "" : "|" + u.getTenantId());
        }
        logger.warn("Creating permission : " + perm.getURI() + " will have mask " + perm.getPermissionMask() + " for " + name);
    }
	
	protected ObjectPermission createObjectPermission(InternalURI target, Object recipient,	int permissionMask) {
			    ObjectPermission permission = getObjectPermissionService().newObjectPermission(null);
			    permission.setURI(target.getURI());
			    permission.setPermissionRecipient(recipient);
			    permission.setPermissionMask(permissionMask);
                logPermission(permission);
			    getObjectPermissionService().putObjectPermission(null, permission);
			    return permission;
			}

	protected ObjectPermission createObjectPermission(String targetPath, Object recipient, int permissionMask) {
			    ObjectPermission permission = getObjectPermissionService().newObjectPermission(null);
			    permission.setURI("repo:" + targetPath);
			    permission.setPermissionRecipient(recipient);
			    permission.setPermissionMask(permissionMask);
			    ExecutionContext ectx = new ExecutionContextImpl();
			    ectx.getAttributes().add(ObjectPermissionServiceImpl.PRIVILEGED_OPERATION);
                logPermission(permission);
			    getObjectPermissionService().putObjectPermission(ectx, permission);
			    return permission;
			}

	protected void deleteObjectPermission(InternalURI target, Object recipient) {
	
	    ObjectPermission op = getObjectPermissionService().newObjectPermission(null);
	
	    String targetURI = target.getURI();
	    op.setURI(targetURI);
	    op.setPermissionRecipient(recipient);
	
	    logger.info("deleteObjectPermission: about to get: " + targetURI + ", recipient: " + recipient);
	    ObjectPermission op2 = getObjectPermissionService().getObjectPermission(null, op);
	    if (op2 != null) {
	    	logger.info("deleteObjectPermission: got " + op2 + ". about to delete: " + targetURI + ", recipient: " + recipient);
	        getObjectPermissionService().deleteObjectPermission(null, op);
	        logger.info("deleted permission for uri: " + targetURI + ", recipient: " + recipient);
	    } else {
	    	logger.info("Can't delete permission for uri: " + targetURI + ", recipient: " + recipient + " because it does not exist");
	    }
	
	}

	public void setTenantService(TenantService tenantService) {
		this.tenantService = tenantService;
	}

	public TenantService getTenantService() {
		return tenantService;
	}

	protected void createTenantForRoot() {
	
	    // create root tenant
	    // setting empty strings in NOT NULL fields (fix for Oracle)
	    createTenant("", TenantService.ORGANIZATIONS, "root", TenantService.ORGANIZATIONS, " ", "/", "/", "default");
	}

	public void createTenant(String parentTenantId, String tenantId, String tenantName,
			String tenantDesc, String tenantNote, String relativeUri, String uri, String theme) {
			
        TenantImpl aTenant = new TenantImpl();

        if (!(TenantService.ORGANIZATIONS.equals(tenantId)) && !(TenantService.ORGANIZATIONS.equals(parentTenantId))) {
            tenantId = parentTenantId + "_" + tenantId;
        }
        aTenant.setParentId(parentTenantId);
        aTenant.setId(tenantId);
        aTenant.setAlias(tenantId);
        aTenant.setTenantName(tenantName);
        aTenant.setTenantDesc(tenantDesc);
        aTenant.setTenantNote(tenantNote);
        aTenant.setTenantUri(relativeUri);   // this is not a true repository URI (describes org hierarchy)
        aTenant.setTenantFolderUri(uri);
        aTenant.setTheme(theme);
			
        tenantService.putTenant(null, aTenant);
			}


	public void setProfileAttributeService(
			ProfileAttributeService profileAttributeService) {
		this.profileAttributeService = profileAttributeService;
	}

	public void setEngineService(EngineService engineService) {
		this.engineService = engineService;
	}

	public EngineService getEngineService() {
		return engineService;
	}

}
