/**
 * Copyright 2005-2014 The Kuali Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
 *
 * 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.
 */
package org.kuali.rice.kew.docsearch;

import org.apache.commons.lang.StringUtils;
import org.joda.time.DateTime;
import org.junit.Test;
import org.kuali.rice.kew.api.KewApiConstants;
import org.kuali.rice.kew.api.KewApiServiceLocator;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kew.api.WorkflowDocumentFactory;
import org.kuali.rice.kew.api.document.DocumentContentUpdate;
import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
import org.kuali.rice.kew.api.extension.ExtensionDefinition;
import org.kuali.rice.kew.docsearch.service.DocumentSearchService;
import org.kuali.rice.kew.docsearch.xml.StandardGenericXMLSearchableAttribute;
import org.kuali.rice.kew.doctype.bo.DocumentType;
import org.kuali.rice.kew.doctype.service.DocumentTypeService;
import org.kuali.rice.kew.impl.document.search.DocumentSearchCriteriaTranslatorImpl;
import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
import org.kuali.rice.kew.routeheader.dao.DocumentRouteHeaderDAO;
import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
import org.kuali.rice.kew.rule.bo.RuleAttribute;
import org.kuali.rice.kew.service.KEWServiceLocator;
import org.kuali.rice.kim.api.identity.Person;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.KRADConstants;

import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.*;

/**
 * Tests the StandardGenericXMLSearchableAttribute.
 *
 * KULWF-654: Tests the resolution to this issue by configuring a CustomActionListAttribute as well as a
 * searchable attribute.
 */
public class SearchableAttributeTest extends DocumentSearchTestBase {

    protected void loadTestData() throws Exception {
        loadXmlFile("SearchAttributeConfig.xml");
        loadXmlFile("SearchableTrimTest.xml");
    }

//    private SearchAttributeCriteriaComponent createSearchAttributeCriteriaComponent(String key,String value,Boolean isLowerBoundValue,DocumentType docType) {
//    	String formKey = (isLowerBoundValue == null) ? key : ((isLowerBoundValue != null && isLowerBoundValue.booleanValue()) ? SearchableAttributeOld.RANGE_LOWER_BOUND_PROPERTY_PREFIX : SearchableAttributeOld.RANGE_UPPER_BOUND_PROPERTY_PREFIX);
//    	String savedKey = key;
//    	SearchAttributeCriteriaComponent sacc = new SearchAttributeCriteriaComponent(formKey,value,savedKey);
//    	Field field = getFieldByFormKey(docType, formKey);
//    	if (field != null) {
//        	sacc.setSearchableAttributeValue(DocSearchUtils.getSearchableAttributeValueByDataTypeString(field.getFieldDataType()));
//        	sacc.setRangeSearch(field.isMemberOfRange());
//        	sacc.setAllowWildcards(field.isAllowingWildcards());
//        	sacc.setAutoWildcardBeginning(field.isAutoWildcardAtBeginning());
//        	sacc.setAutoWildcardEnd(field.isAutoWildcardAtEnding());
//        	sacc.setCaseSensitive(field.isCaseSensitive());
//        	sacc.setSearchInclusive(field.isInclusive());
//            sacc.setSearchable(field.isSearchable());
//            sacc.setCanHoldMultipleValues(Field.MULTI_VALUE_FIELD_TYPES.contains(field.getFieldType()));
//    	}
//    	return sacc;
//    }
//
//    private Field getFieldByFormKey(DocumentType docType, String formKey) {
//    	if (docType == null) {
//    		return null;
//    	}
//		for (SearchableAttributeOld searchableAttribute : docType.getSearchableAttributesOld()) {
//			for (Row row : searchableAttribute.getSearchingRows()) {
//				for (Field field : row.getFields()) {
//					if (field.getPropertyName().equals(formKey)) {
//						return field;
//					}
//				}
//			}
//		}
//		return null;
//    }

    /**
     * This tests the ability to get the searchableAttributeValues directly without going through the document.
     */
    @Test public void testSearchableAttributeSearch()throws Exception {
    	String documentTypeName = "SearchDocType";
        String userNetworkId = "rkirkend";
        WorkflowDocument workflowDocument = WorkflowDocumentFactory.createDocument(getPrincipalId(userNetworkId), documentTypeName);
        workflowDocument.setTitle("Routing style");
        workflowDocument.route("routing this document.");

        workflowDocument = WorkflowDocumentFactory.loadDocument(getPrincipalId(userNetworkId), workflowDocument.getDocumentId());
        DocumentRouteHeaderValue doc = KEWServiceLocator.getRouteHeaderService().getRouteHeader(workflowDocument.getDocumentId());

        // HACK: we are cheating, but this functionality was removed from the service apis, so we hit the DAO directly
        DocumentRouteHeaderDAO dao = KEWServiceLocator.getBean("enDocumentRouteHeaderDAO");
        Collection<SearchableAttributeValue> allValues = dao.findSearchableAttributeValues(workflowDocument.getDocumentId());
        assertEquals("Wrong number of searchable attributes", 4, allValues.size());

        for (SearchableAttributeValue attributeValue: allValues) {
            if (attributeValue instanceof SearchableAttributeStringValue) {
                SearchableAttributeStringValue realValue = (SearchableAttributeStringValue) attributeValue;

                for(String value:getRouteHeaderService().getSearchableAttributeStringValuesByKey(doc.getDocumentId(), realValue.getSearchableAttributeKey())){
                	assertEquals("Assert that the values are the same", value, attributeValue.getSearchableAttributeValue());
                }

            } else if (attributeValue instanceof SearchableAttributeLongValue) {
                SearchableAttributeLongValue realValue = (SearchableAttributeLongValue) attributeValue;
                for(Long value:getRouteHeaderService().getSearchableAttributeLongValuesByKey(doc.getDocumentId(), realValue.getSearchableAttributeKey())){
                	assertEquals("Assert that the values are the same", value, attributeValue.getSearchableAttributeValue());
                }
            } else if (attributeValue instanceof SearchableAttributeFloatValue) {
                SearchableAttributeFloatValue realValue = (SearchableAttributeFloatValue) attributeValue;
                for(BigDecimal value:getRouteHeaderService().getSearchableAttributeFloatValuesByKey(doc.getDocumentId(), realValue.getSearchableAttributeKey())){
                	assertEquals("Assert that the values are the same", value, attributeValue.getSearchableAttributeValue());
                }

            } else if (attributeValue instanceof SearchableAttributeDateTimeValue) {
                SearchableAttributeDateTimeValue realValue = (SearchableAttributeDateTimeValue) attributeValue;
                assertEquals("The only DateTime attribute that should have been added has key '" + TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY + "'", TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, realValue.getSearchableAttributeKey());

                Calendar testDate = Calendar.getInstance();
                testDate.setTimeInMillis(realValue.getSearchableAttributeValue().getTime());
                testDate.set(Calendar.SECOND, 0);
                testDate.set(Calendar.MILLISECOND, 0);

                for(Timestamp value:getRouteHeaderService().getSearchableAttributeDateTimeValuesByKey(doc.getDocumentId(), realValue.getSearchableAttributeKey())){
                	Calendar attributeDate = Calendar.getInstance();
                    attributeDate.setTimeInMillis(value.getTime());
                    attributeDate.set(Calendar.SECOND, 0);
                    attributeDate.set(Calendar.MILLISECOND, 0);

                    assertEquals("The month value for the searchable attribute is wrong",testDate.get(Calendar.MONTH),attributeDate.get(Calendar.MONTH));
                    assertEquals("The date value for the searchable attribute is wrong",testDate.get(Calendar.DATE),attributeDate.get(Calendar.DATE));
                    assertEquals("The year value for the searchable attribute is wrong",testDate.get(Calendar.YEAR),attributeDate.get(Calendar.YEAR));
                }

            } else {
                fail("Searchable Attribute Value base class should be one of the four checked always");
            }
        }
    }

    protected RouteHeaderService getRouteHeaderService(){
    	RouteHeaderService rRet = KEWServiceLocator.getRouteHeaderService();
    	return rRet;
    }

    protected String getPrincipalId(String networkId){
    	return KimApiServiceLocator.getPersonService().getPersonByPrincipalName(networkId).getPrincipalId();
    }

    @Test public void testCustomSearchableAttributesWithDataType() throws Exception {
        String documentTypeName = "SearchDocType";
    	DocumentType docType = ((DocumentTypeService)KEWServiceLocator.getService(KEWServiceLocator.DOCUMENT_TYPE_SERVICE)).findByName(documentTypeName);
        String userNetworkId = "rkirkend";
        WorkflowDocument workflowDocument = WorkflowDocumentFactory.createDocument(getPrincipalId(userNetworkId), documentTypeName);
        workflowDocument.setTitle("Routing style");
        workflowDocument.route("routing this document.");

        workflowDocument = WorkflowDocumentFactory.loadDocument(getPrincipalId(userNetworkId), workflowDocument.getDocumentId());
        DocumentRouteHeaderValue doc = KEWServiceLocator.getRouteHeaderService().getRouteHeader(workflowDocument.getDocumentId());

        DocumentSearchService docSearchService = (DocumentSearchService) KEWServiceLocator.getService(KEWServiceLocator.DOCUMENT_SEARCH_SERVICE);
        Person user = KimApiServiceLocator.getPersonService().getPersonByPrincipalName(userNetworkId);

        DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY,
                TestXMLSearchableAttributeString.SEARCH_STORAGE_VALUE);
        DocumentSearchResults results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals("Search results should have one document.", 1, results.getSearchResults().size());

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, TestXMLSearchableAttributeString.SEARCH_STORAGE_KEY, "fred");
        results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals("Search results should be empty.", 0, results.getSearchResults().size());

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, "fakeproperty", "doesntexist");
        try {
            docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
            fail("Search results should be throwing a validation exception for use of non-existant searchable attribute");
        } catch (RuntimeException e) {}

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY,
                TestXMLSearchableAttributeLong.SEARCH_STORAGE_VALUE.toString());
        results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals("Search results should have one document.", 1, results.getSearchResults().size());

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, TestXMLSearchableAttributeLong.SEARCH_STORAGE_KEY, "1111111");
        results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals("Search results should be empty.", 0, results.getSearchResults().size());

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, "fakeymcfakefake", "99999999");
        try {
            docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
            fail("Search results should be throwing a validation exception for use of non-existant searchable attribute");
        } catch (RuntimeException e) {}

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY,
                TestXMLSearchableAttributeFloat.SEARCH_STORAGE_VALUE.toString());
        results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals("Search results should have one document.", 1, results.getSearchResults().size());

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, TestXMLSearchableAttributeFloat.SEARCH_STORAGE_KEY, "215.3548");
        results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals("Search results should be empty.", 0, results.getSearchResults().size());

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, "fakeylostington", "9999.9999");
        try {
            docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
            fail("Search results should be throwing a validation exception for use of non-existant searchable attribute");
        } catch (RuntimeException e) {}

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY,
                DocumentSearchInternalUtils.getDisplayValueWithDateOnly(new Timestamp(
                        TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_VALUE_IN_MILLS)));
        results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals("Search results should have one document.", 1, results.getSearchResults().size());

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, "07/06/1979");
        results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals("Search results should be empty.", 0, results.getSearchResults().size());

        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        addSearchableAttribute(criteria, "lastingsfakerson", "07/06/2007");
        try {
            docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
            fail("Search results should be throwing a validation exception for use of non-existant searchable attribute");
        } catch (RuntimeException e) {}
    }

    /**
     * Tests searching documents with searchable attributes
     * @throws org.kuali.rice.kew.api.exception.WorkflowException
     */
    @Test public void testSearchAttributesAcrossDocumentTypeVersions() throws Exception {
        // first test searching for an initial version of the doc which does not have a searchable attribute
        loadXmlFile("testdoc0.xml");

        String documentTypeName = "SearchDoc";
        WorkflowDocument doc = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("arh14"), documentTypeName);
        DocumentType docType = ((DocumentTypeService)KEWServiceLocator.getService(KEWServiceLocator.DOCUMENT_TYPE_SERVICE)).findByName(documentTypeName);
        doc.route("routing");

        DocumentSearchService docSearchService = (DocumentSearchService) KEWServiceLocator.getService(KEWServiceLocator.DOCUMENT_SEARCH_SERVICE);

        DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        criteria.setDateCreatedFrom(new DateTime(2004, 1, 1, 0, 0));

        Person user = KimApiServiceLocator.getPersonService().getPersonByPrincipalName("arh14");
        DocumentSearchResults results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals(1, results.getSearchResults().size());

        // now upload the new version with a searchable attribute
        loadXmlFile("testdoc1.xml");
        docType = ((DocumentTypeService)KEWServiceLocator.getService(KEWServiceLocator.DOCUMENT_TYPE_SERVICE)).findByName(documentTypeName);

        // route a new doc
        doc = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("arh14"), documentTypeName);
        doc.route("routing");

        // with no attribute criteria, both docs should be found
        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        criteria.setDateCreatedFrom(new DateTime(2004, 1, 1, 0, 0));

        results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals(2, results.getSearchResults().size());

        // search with specific SearchableAttributeOld value
        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        criteria.setDateCreatedFrom(new DateTime(2004, 1, 1, 0, 0));
        addSearchableAttribute(criteria, "MockSearchableAttributeKey", "MockSearchableAttributeValue");

        results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());
        assertEquals(1, results.getSearchResults().size());

        // search with any SearchableAttributeOld value
        criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        criteria.setDateCreatedFrom(new DateTime(2004, 1, 1, 0, 0));

        results = docSearchService.lookupDocuments(user.getPrincipalId(), criteria.build());

        assertEquals(2, results.getSearchResults().size());
    }

    /**
     * Tests the usage of wildcards on searchable attributes of varying data types.
     * Note that the bounds of ".."-related search expressions will not throw an exception if the lower bound is greater than the upper bound;
     * instead, such an expression will simply return zero results.
     * @throws Exception
     */
    @Test public void testWildcardsOnSearchableAttributes() throws Exception {
        String documentTypeName = "WildcardTestDocType";
    	DocumentType docType = KEWServiceLocator.getDocumentTypeService().findByName(documentTypeName);
        String principalName = "rkirkend";
        String principalId = KimApiServiceLocator.getPersonService().getPersonByPrincipalName(principalName).getPrincipalId();
        String[][] searchableAttributeValuesAsStrings = { {"testString", "9984", "38.1357", "06/24/2009"},
        		{"anotherStr", "33", "80000.65432", "07/08/2010"}, {"MoreText", "432", "-0.765", "12/12/2012"} };

        // Route some documents containing the searchable attribute values given by the above array.
        for (int i = 0; i < searchableAttributeValuesAsStrings.length; i++) {
        	WorkflowDocument workflowDocument = WorkflowDocumentFactory.createDocument(principalId, documentTypeName);

        	// Add the string searchable attribute.
        	WorkflowAttributeDefinition.Builder wcStringXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeWildcardString");
        	wcStringXMLDef.addPropertyDefinition("xmlSearchableAttributeWildcardString", searchableAttributeValuesAsStrings[i][0]);
        	workflowDocument.addSearchableDefinition(wcStringXMLDef.build());
        	// Add the long searchable attribute.
        	WorkflowAttributeDefinition.Builder wcLongXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeWildcardLong");
        	wcLongXMLDef.addPropertyDefinition("xmlSearchableAttributeWildcardLong", searchableAttributeValuesAsStrings[i][1]);
        	workflowDocument.addSearchableDefinition(wcLongXMLDef.build());
        	// Add the float searchable attribute.
        	WorkflowAttributeDefinition.Builder wcFloatXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeWildcardFloat");
        	wcFloatXMLDef.addPropertyDefinition("xmlSearchableAttributeWildcardFloat", searchableAttributeValuesAsStrings[i][2]);
        	workflowDocument.addSearchableDefinition(wcFloatXMLDef.build());
        	// Add the datetime searchable attribute.
        	WorkflowAttributeDefinition.Builder wcDatetimeXMLDef = WorkflowAttributeDefinition.Builder.create("XMLSearchableAttributeWildcardDatetime");
        	wcDatetimeXMLDef.addPropertyDefinition("xmlSearchableAttributeWildcardDatetime", searchableAttributeValuesAsStrings[i][3]);
        	workflowDocument.addSearchableDefinition(wcDatetimeXMLDef.build());

        	workflowDocument.setTitle("Search Def Test Doc " + i);
        	workflowDocument.route("routing search def doc " + i);
        }

        // Ensure that wildcards work on searchable string attributes. Note that this search should be case-insensitive by default.
        // Also note that this should be the only case where the string-specific wildcards ("!", "?", and "*") should be working, unless
        // they are being used in a range expression.
        assertSearchableAttributeWildcardsWork(docType, principalId, "xmlSearchableAttributeWildcardString",
        		new String[]  {"TESTSTRING|moretext", "!MoreText"   , "!anotherStr!testString", "!anotherStr&&!MoreText"  , "!SomeString"      ,
        					"*str*"                 , "More????"    , "*e*n?"                 , "???String"               , "*te*&&!????String", "!test??????"       , "anotherStr..MoreText",
        					"testString..MoreText"  , ">=testString", "<=anotherStr|>MoreText", "<=testString&&!anotherStr", ">=abc"             , "<=anotherOne&&>=text",
        					">=More????"             , "<=*test*"},
        			new int[] {2                    , 2             , 1                       , 1                         , 3                  ,
        					2                       , 1             , 1                       , 0                         , 1                  , 2                   , 2 /*1*/               ,
        					-1                       , 1             , 2                       , 2                         , 3                  , 0                   ,
        					2                       , 2});

        // ensure multiple values work
        assertSearchableAttributeMultiplesWork(docType, principalId, "xmlSearchableAttributeWildcardString",
        		new String[][] { {"testString"}, {"anotherStr"}, {"MoreText"}, {"testString", "anotherStr"}, {"testString", "MoreText"}, {"anotherStr", "MoreText"}, {"testString", "anotherStr", "MoreText"}, {"monkey"}, {"monkey", "giraffe"}, {"monkey", "testString"} },
        			new int[]  {  1,              1,              1,            2,                            2,                          2,                          3,                                        0,          0,                     1                       });

        // Ensure that wildcards work on searchable long attributes, and ensure the string-specific wildcards are not being utilized.
        assertSearchableAttributeWildcardsWork(docType, principalId, "xmlSearchableAttributeWildcardLong",
        		new String[]  {"99??", "*2"       , "!33"         , "<9984", ">=433", "<34", ">=432", ">=34&&<9984", "<100000&&>=20", ">=9984&&<33", "431..<9985",
        					"9999..1", "<432|>432", ">=9000|<=100", "!", ">=-76"},
        			new int[] {-1     , -1          , 2             , 2      , 1     , 1     , 2      , 1           , 3               , 0           , 2 /*1*/    ,
        					-1        , 2          , 2             , -1 , 3});

        // ensure multiple values work
        assertSearchableAttributeMultiplesWork(docType, principalId, "xmlSearchableAttributeWildcardLong",
        		new String[][] { {"9984"}, {"33"}, {"432"}, {"9984", "33"}, {"9984", "432"}, {"33", "432"}, {"9984", "33", "432"}, {"7"}, {"7", "4488"}, {"7", "9984"} },
        			new int[]  {  1,              1,              1,            2,                            2,                          2,                          3,                                        0,          0,                     1                       });

        // Ensure that wildcards work on searchable float attributes, and ensure the string-specific wildcards are not being utilized.
        assertSearchableAttributeWildcardsWork(docType, principalId, "xmlSearchableAttributeWildcardFloat",
        		new String[]  {"38.1???", "!-0.765", "*80*"                , "<80000.65432"   , ">=0"                  , "<-0.763", ">=38.1357", "<38.1358", "<-0.5|>0.5", ">=-0.765&&<=-0.765", ">=38.1358&&<80000.65432",
        					"-50..<50"   , "100..10", "<38.1358|>=38.1357" , ">=123.4567|<0.11", "-1.1..<38.1357&&<3.3"},
        			new int[] {-1        , 2        , -1                     , 2                , 2                     , 1         , 2          , 2         , 3           , 1                   , 0                       ,
        					2           , -1        , 3                     , 2                , 1});

        // ensure multiple values work
        assertSearchableAttributeMultiplesWork(docType, principalId, "xmlSearchableAttributeWildcardFloat",
        		new String[][] { {"38.1357"}, {"80000.65432"}, {"-0.765"}, {"38.1357", "80000.65432"}, {"38.1357", "-0.765"}, {"80000.65432", "-0.765"}, {"38.1357", "80000.65432", "-0.765"}, {"3.1415928"}, {"3.1415928", "4488.0"}, {"3.1415928", "38.1357"} },
        			new int[]  {  1,              1,              1,            2,                            2,                          2,                          3,                                        0,          0,                     1                       });


        // Ensure that wildcards work on searchable datetime attributes, and ensure the string-specific wildcards are not being utilized.
        /* 06/24/2009, 07/08/2010, 12/12/2012 */
        assertSearchableAttributeWildcardsWork(docType, principalId, "xmlSearchableAttributeWildcardDatetime",
        		new String[]  {"??/??/20??"            , "12/12/20*"               , "!07/08/2010"           , ">=06/25/2009", "<07/08/2010", ">=12/12/2012", "<05/06/2011", ">=06/25/2009&&<07/09/2010",
        					">=01/01/2001&&<06/24/2009", "11/29/1990..<12/31/2009"  , "12/13/2100..<08/09/1997",
        					"<06/24/2009|>=12/12/2012" , "<06/25/2009|>=07/09/2010", ">02/31/2011"},
        			new int[] {-1                      , -1                         , 2 /* supports NOT operator*/, 2            , 1            , 1             , 2             , 1                          ,
        					0                          , 1                         , -1                       ,
        					1                          , 2                         , -1});
        
        // ensure multiple values work
        assertSearchableAttributeMultiplesWork(docType, principalId, "xmlSearchableAttributeWildcardDatetime",
        		new String[][] { {"06/24/2009"}, {"07/08/2010"}, {"12/12/2012"}, {"06/24/2009", "07/08/2010"}, {"06/24/2009", "12/12/2012"}, {"07/08/2010", "12/12/2012"}, {"06/24/2009", "07/08/2010", "12/12/2012"}, {"12/20/2012"}, {"12/20/2012", "11/09/2009"}, {"12/20/2012", "12/12/2012"} },
        			new int[]  {  1,              1,              1,            2,                            2,                          2,                          3,                                        0,          0,                     1                       });

    }

    /**
     * A convenience method for testing wildcards on searchable attributes.
     *
     * @param docType The document type containing the attributes.
     * @param principalId The ID of the user performing the search.
     * @param fieldDefKey The name of the field given by the field definition on the searchable attribute.
     * @param searchValues The wildcard-filled search strings to test.
     * @param resultSizes The number of expected documents to be returned by the search; use -1 to indicate that an error should have occurred.
     * @throws Exception
     */
    private void assertSearchableAttributeWildcardsWork(DocumentType docType, String principalId, String fieldDefKey, String[] searchValues,
    		int[] resultSizes) throws Exception {
    	DocumentSearchCriteria.Builder criteria = null;
        DocumentSearchResults results = null;
        DocumentSearchService docSearchService = KEWServiceLocator.getDocumentSearchService();
        for (int i = 0; i < resultSizes.length; i++) {
        	criteria = DocumentSearchCriteria.Builder.create();
        	criteria.setDocumentTypeName(docType.getName());
        	addSearchableAttribute(criteria, fieldDefKey, searchValues[i]);
        	try {
        		results = docSearchService.lookupDocuments(principalId, criteria.build());
        		if (resultSizes[i] < 0) {
        			fail(fieldDefKey + "'s search at loop index " + i + " should have thrown an exception");
        		}
        		if(resultSizes[i] != results.getSearchResults().size()){
        			assertEquals(fieldDefKey + "'s search results at loop index " + i + " returned the wrong number of documents.", resultSizes[i], results.getSearchResults().size());
        		}
        	} catch (Exception ex) {
        		if (resultSizes[i] >= 0) {
                    ex.printStackTrace();
        			fail(fieldDefKey + "'s search at loop index " + i + " should not have thrown an exception");
        		}
        	}
        	GlobalVariables.clear();
        }
    }
    
    /**
     * A convenience method for testing multiple value fields on searchable attributes.
     *
     * @param docType The document type containing the attributes.
     * @param principalId The ID of the user performing the search.
     * @param fieldDefKey The name of the field given by the field definition on the searchable attribute.
     * @param searchValues The wildcard-filled search strings to test.
     * @param resultSizes The number of expected documents to be returned by the search; use -1 to indicate that an error should have occurred.
     * @throws Exception
     */
    private void assertSearchableAttributeMultiplesWork(DocumentType docType, String principalId, String fieldDefKey, String[][] searchValues,
    		int[] resultSizes) throws Exception {
        DocumentSearchCriteria.Builder criteria = null;
        DocumentSearchResults results = null;
        DocumentSearchService docSearchService = KEWServiceLocator.getDocumentSearchService();
        for (int i = 0; i < resultSizes.length; i++) {
            criteria = DocumentSearchCriteria.Builder.create();
        	criteria.setDocumentTypeName(docType.getName());
        	addSearchableAttribute(criteria, fieldDefKey, searchValues[i]);
        	try {
                results = docSearchService.lookupDocuments(principalId, criteria.build());
                if (resultSizes[i] < 0) {
        			fail(fieldDefKey + "'s search at loop index " + i + " should have thrown an exception");
        		}
        		if(resultSizes[i] != results.getSearchResults().size()){
        			assertEquals(fieldDefKey + "'s search results at loop index " + i + " returned the wrong number of documents.", resultSizes[i], results.getSearchResults().size());
        		}
        	} catch (Exception ex) {
        		if (resultSizes[i] >= 0) {
        			fail(fieldDefKey + "'s search at loop index " + i + " should not have thrown an exception");
        		}
        	}
        	GlobalVariables.clear();
        }
    }
    
    
    
    /**
     * Per KULRICE-3681, tests that StandardGenericXMLSearchableAttribute throws no cast class exception when it shouldn't
     */
    @Test
    public void testValidateUserSearchInputsNoCast() {
    	StandardGenericXMLSearchableAttribute searchableAttribute = new StandardGenericXMLSearchableAttribute();
        ExtensionDefinition extensionDefinition = KewApiServiceLocator.getExtensionRepositoryService().getExtensionByName("SearchableAttributeVisible");

        DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create();
    	Map<String, List<String>> simpleParamMap = new HashMap<String, List<String>>();
    	simpleParamMap.put("givenname", Collections.singletonList("test"));
        criteria.setDocumentAttributeValues(simpleParamMap);
    	List errors = new ArrayList();
    	Exception caughtException = null;
    	try {
    		errors = searchableAttribute.validateDocumentAttributeCriteria(extensionDefinition, criteria.build());
    	} catch (RuntimeException re) {
    		caughtException = re;
    	}
    	assertNull("Found exception "+caughtException, caughtException);
    	assertTrue("Found errors "+errors, (errors.size() == 0));
    	
    	Map<String, List<String>> listParamMap = new HashMap<String, List<String>>();
    	List<String> multipleValues = new ArrayList<String>();
    	multipleValues.add("testone");
    	multipleValues.add("testtwo");
    	listParamMap.put("givenname", multipleValues);
        criteria.setDocumentAttributeValues(listParamMap);
    	errors = new ArrayList();
    	caughtException = null;
    	try {
    		errors = searchableAttribute.validateDocumentAttributeCriteria(extensionDefinition, criteria.build());
    	} catch (RuntimeException re) {
    		caughtException = re;
    	}
    	assertNull("Found exception "+caughtException, caughtException);
    	assertTrue("Found errors "+errors, (errors.size() == 0));
    	
    }
    
    @Test
    public void testSearchableAttributeTrim() {
    	RuleAttribute trimAttribute = KEWServiceLocator.getRuleAttributeService().findByName("TrimSearchableAttribute");
    	assert(trimAttribute.getName().equals("TrimSearchableAttribute"));
    	assert(trimAttribute.getResourceDescriptor().equals("org.kuali.rice.kew.docsearch.xml.StandardGenericXMLSearchableAttribute"));
    	assert(trimAttribute.getLabel().equals("Unit111"));
    	assert(trimAttribute.getType().equals("SearchableXmlAttribute"));
    	assert(trimAttribute.getDescription().equals("Unit111"));
    	assert(trimAttribute.getApplicationId().equals("NSTrimSearchableTest"));
    	//System.out.println(trimAttribute.getName());
    }

    @Test
    public void testXmlGeneration() {
        loadXmlFile("testdoc1.xml");
        WorkflowAttributeDefinition searchableDefinition = WorkflowAttributeDefinition.Builder.create("SearchAttribute").build();
        DocumentContentUpdate.Builder documentContentUpdateBuilder = DocumentContentUpdate.Builder.create();
        documentContentUpdateBuilder.getSearchableDefinitions().add(searchableDefinition);
        WorkflowDocument workflowDocument = WorkflowDocumentFactory.createDocument(getPrincipalId("ewestfal"), "SearchDoc", null, documentContentUpdateBuilder.build());
        workflowDocument.route("");
        assertTrue(workflowDocument.isFinal());
        assertEquals(StringUtils.deleteWhitespace("<" + KewApiConstants.SEARCHABLE_CONTENT_ELEMENT + ">" + MockSearchableAttribute.SEARCH_CONTENT + "</" + KewApiConstants.SEARCHABLE_CONTENT_ELEMENT + ">"),
                StringUtils.deleteWhitespace(workflowDocument.getDocumentContent().getSearchableContent()));
    }

    @Test
    public void testAttributeRangeFieldGeneration() {
        String documentTypeName = "SearchDocType";
        DocumentType docType = ((DocumentTypeService)KEWServiceLocator.getService(KEWServiceLocator.DOCUMENT_TYPE_SERVICE)).findByName(documentTypeName);
        String userNetworkId = "rkirkend";
        WorkflowDocument workflowDocument = WorkflowDocumentFactory.createDocument(getPrincipalId(userNetworkId), documentTypeName);
        workflowDocument.setTitle("Routing style");
        workflowDocument.route("routing this document.");

        DocumentSearchCriteria.Builder criteria = DocumentSearchCriteria.Builder.create();
        criteria.setDocumentTypeName(documentTypeName);
        criteria.setDateApprovedFrom(new DateTime(2010, 1, 1, 0, 0));
        criteria.setDateApprovedTo(new DateTime(2011, 1, 1, 0, 0));
        String fieldValue = ">= " + DocumentSearchInternalUtils.getDisplayValueWithDateOnly(new Timestamp(TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_VALUE_IN_MILLS));
        addSearchableAttribute(criteria, TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY, fieldValue);

        Map<String, String[]> fields = new DocumentSearchCriteriaTranslatorImpl().translateCriteriaToFields(criteria.build());
        System.err.println(fields);
        String lowerBoundField = KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX + TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY;
        String upperBoundField = KewApiConstants.DOCUMENT_ATTRIBUTE_FIELD_PREFIX + TestXMLSearchableAttributeDateTime.SEARCH_STORAGE_KEY;

        assertNotNull(fields.get(lowerBoundField));
        assertNotNull(fields.get(upperBoundField));
        assertNotNull(fields.get(lowerBoundField)[0]);
        assertNull(fields.get(upperBoundField)[0]);

        assertEquals(DocumentSearchInternalUtils.getDisplayValueWithDateOnly(new Timestamp(new DateTime(2007, 3, 15, 0, 0).toDateTime().getMillis())), fields.get(lowerBoundField)[0]);
    }
    
}
