package be.iminds.ilabt.jfed.lowlevel.api.test;

import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.api.*;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnectionProvider;
import be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import be.iminds.ilabt.jfed.util.TargetAuthority;

import javax.inject.Inject;
import java.text.ParseException;
import java.util.*;

/**
 * TestFederationSliceAuthority1
 */
public class TestFederationSliceAuthority1 extends AbstractFederationApi1Test {
    @Inject
    public TestFederationSliceAuthority1(Logger logger, TargetAuthority testedAuthority, GeniUserProvider geniUserProvider, JFedConnectionProvider connectionProvider, TestbedInfoSource testbedInfoSource, JFedPreferences jFedPreferences) {
        super(logger, testedAuthority, geniUserProvider, connectionProvider, testbedInfoSource, jFedPreferences);
    }

    public String getTestDescription() {
        return "Test Uniform Federation API Slice Authority";
    }

    private FederationSliceAuthorityApi1 sa;
    private FederationMemberAuthorityApi1 ma;


    public SfaConnection getSAConnection() throws JFedException {
        return (SfaConnection) connectionProvider.getConnectionByAuthority(user, testedAuthority.getServerToConnect(),
                new ApiInfo.Api(ApiInfo.ApiName.GENI_CH_SA, 1));
    }
    public SfaConnection getMAConnection() throws JFedException {
        return (SfaConnection) connectionProvider.getConnectionByAuthority(user, testedAuthority.getServerToConnect(),
                new ApiInfo.Api(ApiInfo.ApiName.GENI_CH_MA, 1));
    }

    final List<String> sliceDefaultFieldNames = new ArrayList<>();
    List<AbstractFederationApi1.GetVersionResult.FieldInfo> sliceDefaultFields = new ArrayList<>();
    final List<String> projectDefaultFieldNames = new ArrayList<>();
    List<AbstractFederationApi1.GetVersionResult.FieldInfo> projectDefaultFields = new ArrayList<>();
    public void setUp() {

        sa = new FederationSliceAuthorityApi1(logger, jFedPreferences);
        ma = new FederationMemberAuthorityApi1(logger, jFedPreferences);
        sa.setHandleMalformedReplies(false); //throw exceptions instead of trying to recover from malformed replies
        ma.setHandleMalformedReplies(false); //throw exceptions instead of trying to recover from malformed replies

        sliceDefaultFields = sa.getMinimumFields("SLICE");
        projectDefaultFields = sa.getMinimumFields("PROJECT");
        for (AbstractFederationApi1.GetVersionResult.FieldInfo fi : sliceDefaultFields)
            sliceDefaultFieldNames.add(fi.getName());
        for (AbstractFederationApi1.GetVersionResult.FieldInfo fi : projectDefaultFields)
            projectDefaultFieldNames.add(fi.getName());
    }


    private boolean hasSliceMemberService = false;
    private boolean hasSliverInfoService = false;
    private boolean hasProjectService = false;
    private boolean hasProjectMemberService = false;

    private List<String> allRoles;
    @Test
    public void getVersion() throws JFedException {
        AbstractFederationApi1.FederationApiReply<FederationSliceAuthorityApi1.GetVersionSAResult> reply = sa.getVersion(getSAConnection());
        checkGetVersion(reply, sa, true);

        Map<String, Object> htRes = assertStringObjectMap(reply.getRawValue());
        List services = assertMapContainsList(htRes, "SERVICES");

        if (services != null) {
            hasSliceMemberService = services.contains("SLICE_MEMBER");
            hasSliverInfoService = services.contains("SLIVER_INFO");
            hasProjectService = services.contains("PROJECT");
            hasProjectMemberService = services.contains("PROJECT_MEMBER");
        }

        allRoles = reply.getValue().getRoles();

        warnIfNot(allRoles.size() > 0, "No roles are defined in get_version");
    }



    AnyCredential maTestUserCredential;
    List<AnyCredential> maTestUserCredentialList;
    @Test(softDepends = {"getVersion"})
    public void getTestUserCredential() throws JFedException {
        GeniUrn testUserUrn = GeniUrn.parse(user.getUserUrnString());
        assertNotNull(testUserUrn, "Error in test user urn: "+user.getUserUrnString());

        AbstractFederationApi1.FederationApiReply<List<AnyCredential>> reply =
                ma.getCredentials(getMAConnection(), new ArrayList<>(), testUserUrn, null);
        checkCorrectnessXmlRpcResult(reply);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        List<AnyCredential> testUserCreds = reply.getValue();
        assertNotNull(testUserCreds, "no credential returned");
        assertNotEmpty(testUserCreds,
                "empty list of credentials returned. Expecting a list with credentials, but got  "+(reply.getRawValue() == null ?
                        "null" :
                        (reply.getRawValue() instanceof Object[] ?
                                ("a list: "+reply.getRawValue()) :
                                ("something of class:"+reply.getRawValue().getClass().getName())
                        )
                ));
        maTestUserCredentialList = new ArrayList<>(testUserCreds);
        maTestUserCredential = testUserCreds.get(0);
    }

    HashMap<String, Object> aggregatedUserInfo;
    @Test(softDepends = {"getTestUserCredential"}, description = "Some methods might require user info, so we try to fetch all user info here. Any failure here is not a failure of the SA, but a failure in the MA.")
    public void getTestUserInfo() throws JFedException {
        GeniUrn testUserUrn = GeniUrn.parse(user.getUserUrnString());
        assertNotNull(testUserUrn, "Error in test user urn: "+user.getUserUrnString());

        Map<String, String> match = new HashMap<>();
        match.put("MEMBER_URN", testUserUrn.getValue());

        aggregatedUserInfo = new HashMap<>();

        setErrorsNotFatal();

        AbstractFederationApi.FederationApiReply<AbstractFederationApi.MemberInfoList> reply =
                ma.lookupPublicMemberInfo(getMAConnection(), maTestUserCredentialList, match, null, null);
        checkCorrectnessXmlRpcResult(reply);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        Map<String,Object> rawHt = assertStringObjectMap(reply.getRawValue());
        if (rawHt!= null) {
            Map<String, Object> partialUserInfo = assertStringObjectMap(rawHt.get(testUserUrn.getValue()));
            if (partialUserInfo != null)
                aggregatedUserInfo.putAll(partialUserInfo);
        }

        reply = ma.lookupIdentifyingMemberInfo(getMAConnection(), maTestUserCredentialList, match, null, null);
        checkCorrectnessXmlRpcResult(reply);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        rawHt = assertStringObjectMap(reply.getRawValue());
        if (rawHt != null) {
            Map<String, Object> partialUserInfo = assertStringObjectMap(rawHt.get(testUserUrn.getValue()));
            if (partialUserInfo != null)
                aggregatedUserInfo.putAll(partialUserInfo);
        }

        reply = ma.lookupPrivateMemberInfo(getMAConnection(), maTestUserCredentialList, match, null, null);
        checkCorrectnessXmlRpcResult(reply);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        rawHt = assertStringObjectMap(reply.getRawValue());
        if (rawHt != null) {
            Map<String, Object> partialUserInfo = assertStringObjectMap(rawHt.get(testUserUrn.getValue()));
            if (partialUserInfo != null)
                aggregatedUserInfo.putAll(partialUserInfo);
        }

        note("All retrieved user info fields: "+aggregatedUserInfo.keySet());

        setErrorsFatal();
    }

    AnyCredential testUserCredential;
    List<AnyCredential> testUserCredentialList;
    @Test(softDepends = {"getTestUserCredential"}, description="Get A Credential for the test user. If the MA get_credential call did not work, this will try to fall back to the old PROTOGENI_SA API.")
    public void retrieveCredentialSomehow() throws JFedException {
        if (maTestUserCredentialList != null && maTestUserCredential != null) {
            testUserCredential = maTestUserCredential;
            testUserCredentialList = maTestUserCredentialList;
        } else {
            if (!ApiInfo.hasService(user.getUserAuthorityServer(), new ApiInfo.Api(ApiInfo.ApiName.PROTOGENI_SA, 1))) {
                errorFatal("The Uniform Federation API Member Authority get_credential call failed to provide a credential. additionally, the test user authority does not have a PROTOGENI_SA available as fallback. => Cannot retrieve a credential for the test user.");
            } else {
                note("The Uniform Federation API Member Authority get_credential call failed to provide a credential: Falling back to the PROTOGENI_SA API to retrieve a credential for the remaining tests.");
                SfaConnection protogeniSaCon = (SfaConnection) connectionProvider.getConnectionByAuthority(user,
                        user.getUserAuthorityServer(), new ApiInfo.Api(ApiInfo.ApiName.PROTOGENI_SA, 1));
                ProtogeniSliceAuthority protogeniSa = new ProtogeniSliceAuthority(logger, jFedPreferences);

                ProtogeniSliceAuthority.SliceAuthorityReply<AnyCredential> pgSaReply = protogeniSa.getCredential(protogeniSaCon);
                assertTrue(pgSaReply.getGeniResponseCode().isSuccess());
                AnyCredential pgCred = pgSaReply.getValue();

                assertNotNull(pgCred, "no credential returned by protogeni SA");
                testUserCredentialList = new ArrayList<>();
                testUserCredentialList.add(pgCred);
                testUserCredential = pgCred;
            }
        }
    }


    private String sliceName;
    private GeniUrn sliceUrn;
    private Map<String,Object> sliceCreationDetails;
    private static final String initialSliceDescription = "test slice used in automated testing";
    private static final Date initialRequestedSliceExpiration = new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000L);
    @Test(hardDepends = {"retrieveCredentialSomehow"}, softDepends = {"getVersion", "createProject", "getTestUserInfo"})
    public void createSlice() throws JFedException, ParseException {
        sliceName = CommonAMTest.createSliceName(this, user.getUserAuthorityServer(), ApiInfo.getDefaultComponentManagerForEach(testedAuthority.getServerForRspecComponentManager()), "S", 3, null);

        Map<String, String> fields = new HashMap<>();
        fields.put("SLICE_EXPIRATION", RFC3339Util.dateToRFC3339String(initialRequestedSliceExpiration, true, true, true));
        fields.put("SLICE_DESCRIPTION", initialSliceDescription);

        //if PROJECT SERVICE is available, we NEED to add SLICE_PROJECT_URN to the keys
        if (hasProjectService) {
            fields.put("SLICE_PROJECT_URN", projectUrn.getValue());
        }
        if (getVersionResult != null && getVersionResult.getFieldsForObject("PROJECT").containsKey("_GENI_SLICE_EMAIL")) {
            if (aggregatedUserInfo != null && aggregatedUserInfo.containsKey("MEMBER_EMAIL"))
                fields.put("_GENI_SLICE_EMAIL", aggregatedUserInfo.get("MEMBER_EMAIL")+"");
            else {
                warn("create_slice requires the field _GENI_SLICE_EMAIL, but the current user's EMAIL is unknown. (the value 'dummy@example.com' will be used, but will likely cause problems)");
                fields.put("_GENI_SLICE_EMAIL", "dummy@example.com");
            }
        }
        addRequiredSupplementaryFieldsToCreate(fields, "SLICE");

        AbstractFederationApi1.FederationApiReply<FederationSliceAuthorityApi1.SliceInfo> reply =
                sa.createSlice(getSAConnection(), testUserCredentialList, sliceName, fields, null);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        sliceCreationDetails = assertStringObjectMap(reply.getRawValue());
        assertNotNull(sliceCreationDetails);
        note("slice details contains the following fields: " + sliceCreationDetails.keySet());

        setErrorsNotFatal();
        assertMapContainsString(sliceCreationDetails, "SLICE_URN");
        String replySliceUrn = (String) sliceCreationDetails.get("SLICE_URN");
        note("SLICE_URN of created slice: \""+replySliceUrn+"\"");
        sliceUrn = GeniUrn.parse(replySliceUrn);
        assertNotNull(sliceUrn, "SLICE_URN value \""+replySliceUrn+"\" is not a valid urn");
        assertEquals(sliceUrn.getEncodedResourceType(), "slice", "SLICE_URN is not a slice urn, but is of type "+sliceUrn.getEncodedResourceType());
        assertEquals(sliceUrn.getEncodedResourceName(), sliceName, "SLICE_URN does not have the requested name \""+sliceName+"\", but has name "+sliceUrn.getEncodedResourceName());
        warnIfNot(sliceUrn.getEncodedTopLevelAuthority().startsWith(testedAuthority.getServerToConnect().getUrnTld()), "SLICE_URN top level authority is unexpected (expected \""+testedAuthority.getServerToConnect().getUrnTld()+"\" or subauthority): "+sliceUrn.getEncodedTopLevelAuthority());

//        sliceUrn = new GeniUrn(testContext.getTestedAuthority().getUrnTld(), "slice", sliceName);

//        assertMapContainsString(sliceCreationDetails, "SLICE_UID");
//        assertMapContainsString(sliceCreationDetails, "SLICE_EXPIRATION"); //XmlRpc might store Data object!
        Object expirationObject = sliceCreationDetails.get("SLICE_EXPIRATION");
        assertNotNull(expirationObject, "slice creation result Map does not contain key \"SLICE_EXPIRATION\"");
        if ((!(expirationObject instanceof Date)) && (!(expirationObject instanceof String)))
            errorNonFatal("slice creation result Map value for \"SLICE_EXPIRATION\" is not a Date or String but a " + expirationObject.getClass().getName());
        setErrorsFatal();

        String receivedSliceExpirationString = (String) expirationObject;
        Date receivedSliceExpirationDate = null;
        try {
            receivedSliceExpirationDate = RFC3339Util.rfc3339StringToDate(receivedSliceExpirationString);

            setErrorsNotFatal();
            assertTrue(receivedSliceExpirationDate.equals(initialRequestedSliceExpiration) || receivedSliceExpirationDate.after(initialRequestedSliceExpiration),
                    "received SLICE_EXPIRATION (" + receivedSliceExpirationString + ") is unexpected " +
                            "given requested SLICE_EXPIRATION (" + RFC3339Util.dateToRFC3339String(initialRequestedSliceExpiration, true, true, true) + ")");
            setErrorsFatal();
        } catch (ParseException e) {
            warn("The returned SLICE_EXPIRATION is NOT a valid RFC3339 date: \""+receivedSliceExpirationString+"\"");
            //throw e;
        }

        //not mandatory
        Object creationSliceCredentialString = sliceCreationDetails.get("SLICE_CREDENTIAL");
        if (creationSliceCredentialString != null) {
            warn("Creating the slice returned a SLICE_CREDENTIAL. This field name is not prefixed with an underscore and is not in the API yet. If it is added, it should probably be of type CREDENTIAL (a list of dictionaries) and not a STRING. Otherwise, the type of credential is not known.");
//            creationSliceCredential = AnyCredential.create("Slice Creation Credential", creationSliceCredentialString, "sfa" , "3"); //assume SFA 3 type...
        }
    }

    private List<AnyCredential> sliceCredentials;
    @Test(hardDepends = {"createSlice"})
    public void getSliceCredentials() throws JFedException {
        AbstractFederationApi1.FederationApiReply<List<AnyCredential>> reply =
                sa.getSliceCredentials(getSAConnection(), testUserCredentialList, sliceUrn, null);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned " + reply.getGeniResponseCode());

        sliceCredentials = reply.getValue();
        assertNotNull(sliceCredentials);
        assertNotEmpty(sliceCredentials);
    }




    private void checkSliceLookupResult(AbstractFederationApi1.FederationApiReply<AbstractFederationApi.SliceInfoList> reply, List<String> expectedFieldNames, List<String> excludedFieldNames) {
        Map<String,Object> lookupRes = assertStringObjectMap(reply.getRawValue());
        checkCorrectnessXmlRpcResult(reply);
        checkLookupCorrectness(reply);
        assertNotNull(lookupRes);

        assertTrue(lookupRes.size() > 0, "Lookup result is empty for slice urn: "+sliceUrn.getValue());

        errorNonFatalIfNot(lookupRes.size() == 1, "lookup must return one slice, but it returned "+ lookupRes.size());

        Map<String, Object> rawUser = assertStringObjectMap(lookupRes.get(sliceUrn.getValue()));
        errorNonFatalIfNot(rawUser != null, "lookup must return requested slice ("+sliceUrn.getValue()+") but returned null.");

        String rawUrn = (String) rawUser.get("SLICE_URN"); //check that if this slice has a slice urn, it matches.
        if (rawUrn != null && !Objects.equals(sliceUrn.getValue(), rawUrn))
            errorNonFatal("lookup must return requested slice ("+sliceUrn.getValue()+"), and it did, however its returned SLICE_URN entry is not matching: "+rawUrn);

        for (Object sliceInfo : lookupRes.values()) {
            final Map<String, Object> rawSliceInfo = assertStringObjectMap(sliceInfo);
            for (String fieldName : expectedFieldNames) {
                if (!rawSliceInfo.containsKey(fieldName))
                    errorNonFatal("returned result does not contain field: \""+ fieldName+"\"");
            }
            for (String fieldName : excludedFieldNames) {
                if (rawSliceInfo.containsKey(fieldName))
                    errorNonFatal("returned result contains unexpected field: \""+ fieldName+"\"");
            }
        }
    }

    @Test(hardDepends = {"getSliceCredentials"})
    public void lookupSlicesNoFilter() throws JFedException {
        Map<String, String> match = new HashMap<>();

        match.put("SLICE_URN", sliceUrn.getValue());

        note("looking up slice by SLICE_URN: " + sliceUrn.getValue());
        AbstractFederationApi1.FederationApiReply<AbstractFederationApi.SliceInfoList> reply =
                sa.lookupSlices(getSAConnection(), sliceCredentials, match, null, null);
        checkCorrectnessXmlRpcResult(reply);

        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());


        List<String> expectedFieldNames = new ArrayList<>();
        List<String> excludedFieldNames = new ArrayList<>();
        for (AbstractFederationApi1.GetVersionResult.FieldInfo fi : sliceDefaultFields)
            if (hasProjectService || !Objects.equals(fi.getName(), "SLICE_PROJECT_URN"))   //special field! only present when project service is supported
                expectedFieldNames.add(fi.getName());
        note("No filter provided, so expecting all fields to be returned: "+expectedFieldNames);
        checkSliceLookupResult(reply, expectedFieldNames, excludedFieldNames);


        final Map<String, Object> rawHt = assertStringObjectMap(reply.getRawValue());
        if(rawHt != null) {
            final Map<String, Object> sliceHt = assertStringObjectMap(rawHt.get(sliceUrn.getValue()));
            String desc = assertMapContainsNonemptyString(sliceHt, "SLICE_DESCRIPTION");
            assertEquals(desc, initialSliceDescription);
        }
    }

    private static final String updatedSliceDescription = "test slice used in automated testing with updated description";
    @Test(hardDepends = {"getSliceCredentials"}, softDepends = {"lookupSlicesNoFilter"})
    public void updateSlice() throws JFedException {
        Map<String, String> fields = new HashMap<>();
        fields.put("SLICE_DESCRIPTION", updatedSliceDescription);

        AbstractFederationApi1.FederationApiReply<String> reply =
                sa.updateSlice(getSAConnection(), sliceCredentials, sliceUrn, fields, null);
        checkCorrectnessXmlRpcResult(reply);

        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());
    }

    @Test(hardDepends = {"updateSlice"})
    public void lookupSlicesNoFilterAfterUpdate() throws JFedException {
        Map<String, String> match = new HashMap<>();

        match.put("SLICE_URN", sliceUrn.getValue());

        note("looking up slice by SLICE_URN: " + sliceUrn.getValue());
        AbstractFederationApi1.FederationApiReply<AbstractFederationApi.SliceInfoList> reply =
                sa.lookupSlices(getSAConnection(), sliceCredentials, match, null, null);
        checkCorrectnessXmlRpcResult(reply);

        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        List<String> expectedFieldNames = new ArrayList<>();
        List<String> excludedFieldNames = new ArrayList<>();
        for (AbstractFederationApi1.GetVersionResult.FieldInfo fi : sliceDefaultFields)
            if (hasProjectService || !Objects.equals(fi.getName(), "SLICE_PROJECT_URN"))   //special field! only present when project service is supported
                expectedFieldNames.add(fi.getName());
        note("No filter provided, so expecting all fields to be returned: "+expectedFieldNames);
        checkSliceLookupResult(reply, expectedFieldNames, excludedFieldNames);

        final Map<String, Object> rawHt = assertStringObjectMap(reply.getRawValue());
        if(rawHt != null) {
            final Map<String, Object> sliceHt = assertStringObjectMap(rawHt.get(sliceUrn.getValue()));
            String desc = assertMapContainsNonemptyString(sliceHt, "SLICE_DESCRIPTION");
            assertEquals(desc, updatedSliceDescription);
        }
    }

    private static final Date requestedRenewedSliceExpiration = new Date(initialRequestedSliceExpiration.getTime() + 4 * 60 * 60 * 1000L);
    @Test(hardDepends = {"getSliceCredentials"}, softDepends = {"lookupSlicesNoFilter"})
    public void renewSlice() throws JFedException {
        Map<String, String> fields = new HashMap<>();
        fields.put("SLICE_EXPIRATION", RFC3339Util.dateToRFC3339String(requestedRenewedSliceExpiration, true, true, true));

        AbstractFederationApi1.FederationApiReply<String> reply =
                sa.updateSlice(getSAConnection(), sliceCredentials, sliceUrn, fields, null);
        checkCorrectnessXmlRpcResult(reply);

        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());
    }

    @Test(hardDepends = {"renewSlice"})
    public void lookupSlicesNoFilterAfterRenew() throws JFedException, ParseException {
        Map<String, String> match = new HashMap<>();

        match.put("SLICE_URN", sliceUrn.getValue());

        note("looking up slice by SLICE_URN: " + sliceUrn.getValue());
        AbstractFederationApi1.FederationApiReply<AbstractFederationApi.SliceInfoList> reply =
                sa.lookupSlices(getSAConnection(), sliceCredentials, match, null, null);
        checkCorrectnessXmlRpcResult(reply);

        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        List<String> expectedFieldNames = new ArrayList<>();
        List<String> excludedFieldNames = new ArrayList<>();
        for (AbstractFederationApi1.GetVersionResult.FieldInfo fi : sliceDefaultFields)
            if (hasProjectService || !Objects.equals(fi.getName(), "SLICE_PROJECT_URN"))   //special field! only present when project service is supported
                expectedFieldNames.add(fi.getName());
        note("No filter provided, so expecting all fields to be returned: "+expectedFieldNames);
        checkSliceLookupResult(reply, expectedFieldNames, excludedFieldNames);

        Map<String,Object> rawHt = assertStringObjectMap(reply.getRawValue());
        final Map<String, Object> stringHt =
                assertStringObjectMap(rawHt.get(sliceUrn.getValue()));
        String expirationString = assertMapContainsNonemptyString(stringHt, "SLICE_EXPIRATION");
        try {
            Date receivedSliceExpiration = RFC3339Util.rfc3339StringToDate(expirationString);

            assertTrue(receivedSliceExpiration.equals(requestedRenewedSliceExpiration) || receivedSliceExpiration.after(requestedRenewedSliceExpiration),
                    "received SLICE_EXPIRATION (" + requestedRenewedSliceExpiration + ") is unexpected " +
                            "given requested renew SLICE_EXPIRATION (" + RFC3339Util.dateToRFC3339String(requestedRenewedSliceExpiration, true, true, true) + ")");
        } catch (ParseException e) {
            errorNonFatal("The returned SLICE_EXPIRATION is NOT a valid RFC3339 date: \""+expirationString+"\"");
            //throw e;
        }
    }




    ////////////////////////////////////////////////// SLICE_MEMBER methods //////////////////////////////////////////////////

    private String lookupSliceMembersRole;
    @Test(hardDepends = {"createSlice", "getSliceCredentials"}, softDepends = {"lookupSlicesNoFilterAfterUpdate"})
    public void lookupSliceMembers() throws JFedException {
        if (!hasSliceMemberService) skip("No SLICE_MEMBER service advertised in get_version");

        AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply =
                sa.lookupSliceMembers(getSAConnection(), sliceUrn, sliceCredentials, null);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        List<FederationSliceAuthorityApi1.UrnRoleTuple> memberTuples = reply.getValue();
        assertNotNull(memberTuples);
        assertNotEmpty(memberTuples);
        assertTrue(memberTuples.size() == 1, "More than 1 member in slice: "+memberTuples);

        FederationSliceAuthorityApi1.UrnRoleTuple memberTuple = memberTuples.get(0);
        assertEquals(memberTuple.urn.getValue(), user.getUserUrnString(), "Expected slice member to be "+user.getUserUrnString()+" but it was "+memberTuple.urn.getValue());
        note("test user role in slice is: "+memberTuple.role);
        assertNotNull(memberTuple.role, "member role is null in slice");
        lookupSliceMembersRole = memberTuple.role;
    }

    @Test(hardDepends = {"createSlice"}, softDepends = {"lookupSliceMembers"})
    public void lookupSlicesForMember() throws JFedException {
        if (!hasSliceMemberService) skip("No SLICE_MEMBER service advertised in get_version");

        GeniUrn memberUrn = GeniUrn.parse(user.getUserUrnString());
        assertNotNull(memberUrn);

        AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply =
                sa.lookupSlicesForMember(getSAConnection(), memberUrn, testUserCredentialList, null);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        List<FederationSliceAuthorityApi1.UrnRoleTuple> value = reply.getValue();

        assertNotNull(value);
        assertNotEmpty(value);
        assertTrue(value.size() >= 1, "should have been at least 1 slice for " + memberUrn);
        note("Slice urns for member "+memberUrn+": "+value);

        String role = null;
        for (FederationSliceAuthorityApi1.UrnRoleTuple tuple : value) {
//            note("comparing \""+tuple.getUrn()+"\" with \""+sliceUrn+"\" -> "+tuple.getUrn().equals(sliceUrn));
            if (tuple.getUrn().getEncodedTopLevelAuthority().startsWith(sliceUrn.getEncodedTopLevelAuthority())
                    && Objects.equals(tuple.getUrn().getEncodedResourceName(), sliceUrn.getEncodedResourceName())
                    && Objects.equals(tuple.getUrn().getEncodedResourceType(), "slice"))
                role = tuple.getRole();
            assertEquals(tuple.getUrn().getEncodedResourceType(), "slice", "The returned URN is not a slice urn but is of type: \""+tuple.getUrn().getEncodedResourceType()+"\"");
        }

        assertNotNull(role, "slice \""+sliceUrn+"\" was not listed as slice for member \""+memberUrn+"\". Full result: "+value);
        if (lookupSliceMembersRole != null)
            assertEquals(role, lookupSliceMembersRole, "A different role was returned for \""+memberUrn+"\" in "+sliceUrn+" for the lookup_slices_for_members and lookup_slice_members calls");
    }

    @Test(hardDepends = {"getVersion", "createSlice", "lookupSliceMembers"}, softDepends = {"lookupSlicesForMember"})
    public void modifySliceMembership() throws JFedException {
        assertNotNull(allRoles, "get_version test did not set allRoles");
        assertNotNull(lookupSliceMembersRole, "lookupSliceMembersRole not set in lookupSliceMembers");

        if (!hasSliceMemberService) skip("No SLICE_MEMBER service advertised in get_version");

        List<FederationSliceAuthorityApi1.UrnRoleTuple> membersToAdd = new ArrayList<>();
        List<GeniUrn> membersToRemove = new ArrayList<>();
        List<FederationSliceAuthorityApi1.UrnRoleTuple> membersToChange = new ArrayList<>();

        if (allRoles.size() <= 1) {
            skip("server does not have multiple roles: cannot test modify_slice_membership");
        }

        String newRole = allRoles.get(0);
        String oldRole = lookupSliceMembersRole;

        if (allRoles.contains("USER") && !Objects.equals(oldRole, "USER")) newRole = "USER";
        if (allRoles.contains("MEMBER") && !Objects.equals(oldRole, "MEMBER")) newRole = "MEMBER";

        for (int i = 1; Objects.equals(newRole, oldRole) && i < allRoles.size(); i++)
            newRole = allRoles.get(i);
        assertNotEquals(newRole, oldRole, "roles defined in get_version do not differ: oldRole="+oldRole+" allRoles="+allRoles);

        membersToChange.add(new FederationSliceAuthorityApi1.UrnRoleTuple(user.getUserUrn(), newRole));
        note("Wil ltry to change role of member \""+user.getUserUrnString()+"\" in slice \""+sliceUrn+"\" from \""+oldRole+"\" to \""+newRole+"\"");

        AbstractFederationApi1.FederationApiReply<String> reply1 =
                sa.modifySliceMembership(getSAConnection(), sliceUrn, membersToAdd, membersToRemove, membersToChange, testUserCredentialList, null);
        checkCorrectnessXmlRpcResult(reply1);

        assertTrue(reply1.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply1.getGeniResponseCode());




        //lookup and check if changed correctly
        AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply2 =
                sa.lookupSliceMembers(getSAConnection(), sliceUrn, testUserCredentialList, null);
        assertTrue(reply2.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply2.getGeniResponseCode());

        List<FederationSliceAuthorityApi1.UrnRoleTuple> memberTuples = reply2.getValue();
        assertNotNull(memberTuples);
        assertNotEmpty(memberTuples);
        assertTrue(memberTuples.size() == 1, "Expected exactly 1 member in slice: "+memberTuples);

        FederationSliceAuthorityApi1.UrnRoleTuple memberTuple = memberTuples.get(0);
        assertEquals(memberTuple.urn.getValue(), user.getUserUrnString(), "Expected slice member to be "+user.getUserUrnString()+" but it was "+memberTuple.urn.getValue());
        note("test user role in slice is: "+memberTuple.role);
        assertNotNull(memberTuple.role, "member role is null in slice");
        assertEquals(memberTuple.role,  newRole, "member role differs from newly assigned role");
        lookupSliceMembersRole = memberTuple.role;
    }


    ////////////////////////////////////////////////// SLIVER_INFO methods //////////////////////////////////////////////////

    @Test(hardDepends = {"retrieveCredentialSomehow", "createSlice"},
            softDepends = {"lookupSlicesNoFilterAfterUpdate", "lookupSlicesForMember", "lookupSliceMembers", "modifySliceMembership"})
    public void lookupSliverInfoNoFilter() throws JFedException {
        if (!hasSliverInfoService) skip("No SLIVER_INFO service advertised in get_version");

        Map<String, String> match = new HashMap<>();

        AbstractFederationApi1.FederationApiReply<AbstractFederationApi.SliverInfoList> reply =
                sa.lookupSliverInfo(getSAConnection(), testUserCredentialList, sliceUrn, match, null, null);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        checkLookupCorrectness(reply);
        AbstractFederationApi.SliverInfoList lookupRes = reply.getValue();
        assertNotNull(lookupRes);
        assertTrue(lookupRes.size() == 0);

        note("This test just checks that there are no slivers in the created slice. Nothing else is currently tested for SLIVER_INFO");
    }



    ////////////////////////////////////////////////// PROJECT methods //////////////////////////////////////////////////

    private String projectName;
    private GeniUrn projectUrn;
    private Map<String, Object> projectCreationDetails;
    private static final String initialProjectDescription = "test project used in automated testing";
    @Test(hardDepends = {"retrieveCredentialSomehow"}, softDepends = {"getVersion", "getTestUserInfo"})
    public void createProject() throws JFedException {
        if (!hasProjectService) skip("No PROJECT service advertised in get_version");

        projectName = "P"+System.currentTimeMillis();
        projectUrn = GeniUrn.createGeniUrnFromEncodedParts(testedAuthority.getServerToConnect().getUrnTld(), "project", projectName);

        Map<String, String> fields = new HashMap<>();
        fields.put("PROJECT_DESCRIPTION", initialProjectDescription);
        fields.put("PROJECT_EXPIRATION", RFC3339Util.dateToRFC3339String(new Date(System.currentTimeMillis() + 3 * 60 * 60 * 1000), true, true, true));

        if (getVersionResult == null)
            warn("get_version result is unknown to this test method. Cannot add any required supplemental fields.");
        else {
            if (getVersionResult.getFields() == null)
                warn("getVersionResult fieldInfos map is null. Bug in jFed?");
            else {
                note("All Required supplemental fields: "+getVersionResult.getFields().keySet());
            }
            if (getVersionResult.getFieldsForObject("PROJECT") == null)
                warn("getVersionResult fieldInfos map is null when called with getFieldsForObject(\"PROJECT\"). Bug in jFed?");
            else {
                note("Required supplemental fields for PROJECT are: "+getVersionResult.getFieldsForObject("PROJECT").keySet());
            }
        }
        if (getVersionResult != null &&  getVersionResult.getFieldsForObject("PROJECT") != null) {
            if (getVersionResult.getFieldsForObject("PROJECT").containsKey("_GENI_PROJECT_EMAIL")) {
                note("get_version contains supplementary field _GENI_PROJECT_EMAIL. Will fill in user email.");
                if (aggregatedUserInfo != null && aggregatedUserInfo.containsKey("MEMBER_EMAIL"))
                    fields.put("_GENI_PROJECT_EMAIL", aggregatedUserInfo.get("MEMBER_EMAIL")+"");
                else {
                    warn("create_project requires the field _GENI_PROJECT_EMAIL, but the current users EMAIL is unknown. (the value 'dummy@example.com' will be used, but will likely cause problems)");
                    fields.put("_GENI_PROJECT_EMAIL", "dummy@example.com");
                }
            }
            if (getVersionResult.getFieldsForObject("PROJECT").containsKey("_GENI_PROJECT_OWNER")) { //type UID
                note("get_version contains supplementary field _GENI_PROJECT_OWNER. Will fill in user UID");
                if (aggregatedUserInfo != null && aggregatedUserInfo.containsKey("MEMBER_UID"))
                    fields.put("_GENI_PROJECT_OWNER", aggregatedUserInfo.get("MEMBER_UID")+"");
                else {
                    warn("create_project requires the field _GENI_PROJECT_OWNER, but the current users UID is unknown. (the value 'unknown' will be used, but will likely cause problems)");
                    fields.put("_GENI_PROJECT_OWNER", "unknown");
                }

            }
        }
        addRequiredSupplementaryFieldsToCreate(fields, "PROJECT");

        AbstractFederationApi.FederationApiReply<AbstractFederationApi.ProjectInfo> reply =
                sa.createProject(getSAConnection(), testUserCredentialList, projectName, fields, null);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        projectCreationDetails = assertStringObjectMap(reply.getRawValue());
        assertNotNull(projectCreationDetails);
        note("project details contains the following fields: " + projectCreationDetails.keySet());

        setErrorsNotFatal();
        assertMapContainsString(projectCreationDetails, "PROJECT_URN");
        setErrorsFatal();

        //TODO: try to set a short expiration time here or in update
    }


    private void checkProjectLookupResult(AbstractFederationApi1.FederationApiReply<AbstractFederationApi.ProjectInfoList> reply, List<String> expectedFieldNames, List<String> excludedFieldNames) {
        Map<String,Object> lookupRes =  assertStringObjectMap(reply.getRawValue());
        checkCorrectnessXmlRpcResult(reply);
        checkLookupCorrectness(reply);
        assertNotNull(lookupRes);

        assertTrue(lookupRes.size() > 0, "Lookup result is empty for project urn: "+projectUrn.getValue());

        errorNonFatalIfNot(lookupRes.size() == 1, "lookup must return one project, but it returned "+lookupRes.size());

        Map<String, Object> rawUser = assertStringObjectMap(lookupRes.get(projectUrn.getValue()));
        errorNonFatalIfNot(rawUser != null, "lookup must return requested project ("+projectUrn.getValue()+") but returned null.");

        String rawUrn = (String) rawUser.get("PROJECT_URN"); //check that if this project has a project urn, it matches.
        if (rawUrn != null && !Objects.equals(projectUrn.getValue(), rawUrn))
            errorNonFatal("lookup must return requested project ("+projectUrn.getValue()+"), and it did, however its returned PROJECT_URN entry is not matching: "+rawUrn);

        String rawName = (String) rawUser.get("PROJECT_NAME"); //check that if this project has a project name, it matches.
        if (rawName != null && !Objects.equals(projectName, rawName))
            errorNonFatal("lookup must return requested project (urn="+projectUrn.getValue()+" name="+projectName+"), and it did, however its returned PROJECT_Name entry is not matching: "+rawName);

        for (Object projectInfo : lookupRes.values()) {
            Map<String, Object> rawProjectInfo = assertStringObjectMap(projectInfo);
            for (String fieldName : expectedFieldNames) {
                if (!rawProjectInfo.containsKey(fieldName))
                    errorNonFatal("returned result does not contain field: \""+ fieldName+"\"");
            }
            for (String fieldName : excludedFieldNames) {
                if (rawProjectInfo.containsKey(fieldName))
                    errorNonFatal("returned result contains unexpected field: \""+ fieldName+"\"");
            }
        }
    }

    @Test(hardDepends = {"createProject"})
    public void lookupProjectsByUrnNoFilter() throws JFedException {
        if (!hasProjectService) skip("No PROJECT service advertised in get_version");

        Map<String, String> match = new HashMap<>();
        List<String> filter = null;//new List<String>();

        match.put("PROJECT_URN", projectUrn.getValue());

        note("looking up project by PROJECT_URN: " + projectUrn.getValue());
        AbstractFederationApi1.FederationApiReply<AbstractFederationApi.ProjectInfoList> reply =
                sa.lookupProjects(getSAConnection(), testUserCredentialList, match, null, null);
        checkCorrectnessXmlRpcResult(reply);

        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        List<String> expectedFieldNames = new ArrayList<>();
        List<String> excludedFieldNames = new ArrayList<>();
        for (AbstractFederationApi1.GetVersionResult.FieldInfo fi : projectDefaultFields)
            expectedFieldNames.add(fi.getName());
        note("No filter provided, so expecting all fields to be returned: "+expectedFieldNames);
        checkProjectLookupResult(reply, expectedFieldNames, excludedFieldNames);

        final Map<String, Object> rawHt = assertStringObjectMap(reply.getRawValue());
        if(rawHt != null) {
            final Map<String, Object> projectHt = assertStringObjectMap(rawHt.get(projectUrn.getValue()));
            String desc = assertMapContainsNonemptyString(projectHt, "PROJECT_DESCRIPTION");
            assertEquals(desc, initialProjectDescription);
        }
    }

    @Test(hardDepends = {"createProject"})
    public void lookupProjectsByNameNoFilter() throws JFedException {
        if (!hasProjectService) skip("No PROJECT service advertised in get_version");

        Map<String, String> match = new HashMap<>();
        List<String> filter = null;//new List<String>();

        match.put("PROJECT_NAME", projectName);

        note("looking up project by PROJECT_URN: " + projectUrn.getValue());
        AbstractFederationApi1.FederationApiReply<AbstractFederationApi.ProjectInfoList> reply =
                sa.lookupProjects(getSAConnection(), testUserCredentialList, match, null, null);
        checkCorrectnessXmlRpcResult(reply);

        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        List<String> expectedFieldNames = new ArrayList<>();
        List<String> excludedFieldNames = new ArrayList<>();
        for (AbstractFederationApi1.GetVersionResult.FieldInfo fi : projectDefaultFields)
            expectedFieldNames.add(fi.getName());
        note("No filter provided, so expecting all fields to be returned: "+expectedFieldNames);
        checkProjectLookupResult(reply, expectedFieldNames, excludedFieldNames);

        final Map<String, Object> rawHt = assertStringObjectMap(reply.getRawValue());
        if(rawHt != null) {
            final Map<String, Object> projectHt = assertStringObjectMap(rawHt.get(projectUrn.getValue()));
            String desc = assertMapContainsNonemptyString(projectHt, "PROJECT_DESCRIPTION");
            assertEquals(desc, initialProjectDescription);
        }
    }

    private static final String updatedProjectDescription = "test project used in automated testing with updated description";
    @Test(hardDepends = {"createProject"}, softDepends = {"lookupProjectsByUrnNoFilter", "lookupProjectsByNameNoFilter"})
    public void updateProject() throws JFedException {
        if (!hasProjectService) skip("No PROJECT service advertised in get_version");

        Map<String, String> fields = new HashMap<>();
        fields.put("PROJECT_DESCRIPTION", updatedProjectDescription);

        AbstractFederationApi1.FederationApiReply<String> reply =
                sa.updateProject(getSAConnection(), testUserCredentialList, projectUrn, fields, null);
        checkCorrectnessXmlRpcResult(reply);

        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());
    }

    @Test(hardDepends = {"updateProject"})
    public void lookupProjectsNoFilterAfterUpdate() throws JFedException {
        if (!hasProjectService) skip("No PROJECT service advertised in get_version");

        Map<String, String> match = new HashMap<>();
        List<String> filter = null;// new List<String>();

        match.put("PROJECT_URN", projectUrn.getValue());

        note("looking up project by PROJECT_URN: " + projectUrn.getValue());
        AbstractFederationApi1.FederationApiReply<AbstractFederationApi.ProjectInfoList> reply =
                sa.lookupProjects(getSAConnection(), testUserCredentialList, match, null, null);
        checkCorrectnessXmlRpcResult(reply);

        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        List<String> expectedFieldNames = new ArrayList<>();
        List<String> excludedFieldNames = new ArrayList<>();
        for (AbstractFederationApi1.GetVersionResult.FieldInfo fi : projectDefaultFields)
            expectedFieldNames.add(fi.getName());
        note("No filter provided, so expecting all fields to be returned: "+expectedFieldNames);
        checkProjectLookupResult(reply, expectedFieldNames, excludedFieldNames);

        final Map<String, Object> rawHt = assertStringObjectMap(reply.getRawValue());
        if(rawHt != null) {
            final Map<String, Object> projectHt = assertStringObjectMap(rawHt.get(projectUrn.getValue()));
            String desc = assertMapContainsNonemptyString(projectHt, "PROJECT_DESCRIPTION");
            assertEquals(desc, updatedProjectDescription);
        }
    }

    ////////////////////////////////////////////////// PROJECT_MEMBER methods //////////////////////////////////////////////////

    private String lookupProjectMembersRole;
    @Test(hardDepends = {"retrieveCredentialSomehow", "createProject"}, softDepends = {"lookupProjectsNoFilterAfterUpdate"})
    public void lookupProjectMembers() throws JFedException {
        if (!hasProjectMemberService) skip("No PROJECT_MEMBER service advertised in get_version");

        AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply =
                sa.lookupProjectMembers(getSAConnection(), projectUrn, testUserCredentialList, null);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        List<FederationSliceAuthorityApi1.UrnRoleTuple> memberTuples = reply.getValue();
        assertNotNull(memberTuples);
        assertNotEmpty(memberTuples);
        assertTrue(memberTuples.size() == 1, "Expected exactly 1 member in project: "+memberTuples);

        FederationSliceAuthorityApi1.UrnRoleTuple memberTuple = memberTuples.get(0);
        assertEquals(memberTuple.urn.getValue(), user.getUserUrnString(), "Expected project member to be "+user.getUserUrnString()+" but it was "+memberTuple.urn.getValue());
        note("test user role in project is: "+memberTuple.role);
        assertNotNull(memberTuple.role, "member role is null in project");
        lookupProjectMembersRole = memberTuple.role;
    }

    @Test(hardDepends = {"createProject"}, softDepends = {"lookupProjectMembers"})
    public void lookupProjectsForMember() throws JFedException {
        if (!hasProjectMemberService) skip("No PROJECT_MEMBER service advertised in get_version");

        GeniUrn memberUrn = GeniUrn.parse(user.getUserUrnString());
        assertNotNull(memberUrn);

        AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply =
                sa.lookupProjectsForMember(getSAConnection(), memberUrn, testUserCredentialList, null);
        assertTrue(reply.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply.getGeniResponseCode());

        List<FederationSliceAuthorityApi1.UrnRoleTuple> value = reply.getValue();

        assertNotNull(value);
        assertNotEmpty(value);
        assertTrue(value.size() >= 1, "should have been at least 1 project for " + memberUrn);
        note("Project urns for member "+memberUrn+": "+value);

        String role = null;
        for (FederationSliceAuthorityApi1.UrnRoleTuple tuple : value) {
            if (Objects.equals(tuple.getUrn(), projectUrn))
                role = tuple.getRole();
            assertEquals(tuple.getUrn().getEncodedResourceType(), "project", "The returned URN is not a project urn but is of type: \""+tuple.getUrn().getEncodedResourceType()+"\"");
        }

        assertNotNull(role, "project "+projectUrn+" was not listed as project for member "+memberUrn+". All: "+value);
        if (lookupProjectMembersRole != null)
            assertEquals(role, lookupProjectMembersRole, "A different role was returned for "+memberUrn+" in "+projectUrn+" for the lookup_projects_for_members and lookup_project_members calls");
    }

    @Test(hardDepends = {"getVersion", "createProject", "lookupProjectMembers"}, softDepends = {"lookupProjectsForMember"})
    public void modifyProjectMembership() throws JFedException {
        assertNotNull(allRoles, "get_version test did not set allRoles");
        assertNotNull(lookupProjectMembersRole, "lookupProjectMembersRole not set in lookupProjectMembers");

        if (!hasProjectMemberService) skip("No PROJECT_MEMBER service advertised in get_version");

        List<FederationSliceAuthorityApi1.UrnRoleTuple> membersToAdd = new ArrayList<>();
        List<GeniUrn> membersToRemove = new ArrayList<>();
        List<FederationSliceAuthorityApi1.UrnRoleTuple> membersToChange = new ArrayList<>();

        if (allRoles.size() <= 1) {
            skip("server does not have multiple roles: cannot test modify_project_membership");
        }

        String newRole = allRoles.get(0);
        String oldRole = lookupProjectMembersRole;

        if (allRoles.contains("USER") && !Objects.equals(oldRole, "USER")) newRole = "USER";
        if (allRoles.contains("MEMBER") && !Objects.equals(oldRole, "MEMBER")) newRole = "MEMBER";

        for (int i = 1; Objects.equals(newRole, oldRole) && i < allRoles.size(); i++)
            newRole = allRoles.get(i);
        assertNotEquals(newRole, oldRole, "roles defined in get_version do not differ: oldRole="+oldRole+" allRoles="+allRoles);

        membersToChange.add(new FederationSliceAuthorityApi1.UrnRoleTuple(user.getUserUrn(), newRole));
        note("Wil ltry to change role of member \""+user.getUserUrnString()+"\" in project \""+projectUrn+"\" from \""+oldRole+"\" to \""+newRole+"\"");

        AbstractFederationApi1.FederationApiReply<String> reply1 =
                sa.modifyProjectMembership(getSAConnection(), projectUrn, membersToAdd, membersToRemove, membersToChange, testUserCredentialList, null);
        checkCorrectnessXmlRpcResult(reply1);

        assertTrue(reply1.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply1.getGeniResponseCode());




        //lookup and check if changed correctly
        AbstractFederationApi1.FederationApiReply<List<FederationSliceAuthorityApi1.UrnRoleTuple>> reply2 =
                sa.lookupProjectMembers(getSAConnection(), projectUrn, testUserCredentialList, null);
        assertTrue(reply2.getGeniResponseCode().isSuccess(), "The tested call did not return code 0 (success), instead it returned "+reply2.getGeniResponseCode());

        List<FederationSliceAuthorityApi1.UrnRoleTuple> memberTuples = reply2.getValue();
        assertNotNull(memberTuples);
        assertNotEmpty(memberTuples);
        assertTrue(memberTuples.size() == 1, "Expected exactly 1 member in project: "+memberTuples);

        FederationSliceAuthorityApi1.UrnRoleTuple memberTuple = memberTuples.get(0);
        assertEquals(memberTuple.urn.getValue(), user.getUserUrnString(), "Expected project member to be "+user.getUserUrnString()+" but it was "+memberTuple.urn.getValue());
        note("test user role in project is: "+memberTuple.role);
        assertNotNull(memberTuple.role, "member role is null in project");
        assertEquals(memberTuple.role,  newRole, "member role differs from newly assigned role");
        lookupProjectMembersRole = memberTuple.role;
    }

}
