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

import be.iminds.ilabt.jfed.call_log_output.HtmlLogOutput;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Proxy;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.UserAndSliceApiWrapper;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnection;
import be.iminds.ilabt.jfed.lowlevel.connection.SfaConnectionPool;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.SshKeyInfo;
import be.iminds.ilabt.jfed.lowlevel.ssh_key_info.SshKeyInfoFactory;
import be.iminds.ilabt.jfed.lowlevel.stitching.VlanRange;
import be.iminds.ilabt.jfed.rspec.model.BasicStringRspec;
import be.iminds.ilabt.jfed.rspec.model.ModelRspecType;
import be.iminds.ilabt.jfed.rspec.model.impl.BasicModelRspec;
import be.iminds.ilabt.jfed.rspec.model.impl.BasicRspecNode;
import be.iminds.ilabt.jfed.rspec.rspec_source.RequestRspecSource;
import be.iminds.ilabt.jfed.testing.base.ApiTest;
import be.iminds.ilabt.jfed.ui.commandline.ContextFile;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.IOUtils;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import be.iminds.ilabt.jfed.util.TextUtil;
import net.schmizz.sshj.connection.channel.direct.Session;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * CommonAMTest:
 * <p/>
 * methods for common tests in AM's
 */
public class CommonAMTest {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(CommonAMTest.class);
    private static final Random RANDOM = new Random(System.currentTimeMillis());
    private static final int MAX_CREATE_SLICE_TRIES = 3;
    private final ApiTest test;
    private final Server userAuthority;
    private final List<Server> testedAuthoritiesForRspec;
    private List<AnyCredential> userCredentials;
    //    private ProtogeniSliceAuthority sa;  //not used directly, use userAndSliceApiWrapperUser1
    private UserAndSliceApiWrapper userAndSliceApiWrapperUser1;
    private UserAndSliceApiWrapper userAndSliceApiWrapperUser2;
    private List<AnyCredential> user2Credentials;
    private TestCredentialType testCredentialType = TestCredentialType.REGULAR;

    private SfaCredential delegatedUserCredential;
    private AnyCredential speaksForUserCredential;
    private final List<SliceInfo> allSliceInfo = new ArrayList<>();

    private SpeaksForConfig speaksForConfig;
    protected User2LoginConfig secondUserConfig;


    public CommonAMTest(@Nonnull ApiTest test) {
        this(test, test.getUser().getUserAuthorityServer(), test.getTestedAuthority().getServerForRspecComponentManager());
    }

    public CommonAMTest(@Nonnull ApiTest test, Server userAuthority) {
        this(test, userAuthority, Collections.singletonList(test.getTestedAuthority().getServerForRspecComponentManager()));
    }

    public CommonAMTest(@Nonnull ApiTest test, Server userAuthority, Server testedAuthorityForRspec) {
        this(test, userAuthority, Collections.singletonList(testedAuthorityForRspec));
    }

    public CommonAMTest(@Nonnull ApiTest test, Server userAuthority, List<Server> testedAuthoritiesForRspec) {
        this.test = test;
        this.userAuthority = userAuthority;
        this.testedAuthoritiesForRspec = testedAuthoritiesForRspec;

        assert this.test != null;
        assert this.userAuthority != null;

//        sa = new ProtogeniSliceAuthority(test.getLogger());

        userAndSliceApiWrapperUser1 =
                test.getAutomaticUserAndSliceApiWrapperFactory().create();
    }

    public static String getComponentManagerIdForSite(Server auth) {
        if (auth.isServerType("emulab"))
            return auth.getDefaultComponentManagerUrn();
        if (auth.isServerType("instageni"))
            return auth.getDefaultComponentManagerUrn();
        if (auth.isServerType("planetlab"))
            return auth.getDefaultComponentManagerUrn();
        if (auth.isServerType("orca") || auth.isServerType("exogeni")) {
            //see https://geni-orca.renci.org/trac/wiki/orca-and-rspec
            String sitename = null;
            if (Objects.equals(auth.getDefaultComponentManagerUrn(), "urn:publicid:IDN+geni.renci.org+authority+cm")) //"ties resources from all"
                sitename = "exogeni.net"; //just a guess, perhaps this is ANY?
            // Note: There is a way to select any, but I do not know the urn: see https://wiki.exogeni.net/doku.php?id=public:experimenters:start#selecting_which_orca_actor_to_request_resources_from
            if (Objects.equals(auth.getDefaultComponentManagerUrn(), "urn:publicid:IDN+bbn-hn.exogeni.net+authority+cm"))
                sitename = "exogeni.net:bbnvmsite";
            if (Objects.equals(auth.getDefaultComponentManagerUrn(), "urn:publicid:IDN+dbc1-16.nicl.cs.duke.edu+authority+cm"))
                sitename = "exogeni.net:dukevmsite";
            if (Objects.equals(auth.getDefaultComponentManagerUrn(), "urn:publicid:IDN+fiu-hn.exogeni.net+authority+cm"))
                sitename = "exogeni.net:fiuvmsite";
            if (Objects.equals(auth.getDefaultComponentManagerUrn(), "urn:publicid:IDN+nicta-hn.exogeni.net+authority+cm"))
                sitename = "exogeni.net:nictavmsite";
            if (Objects.equals(auth.getDefaultComponentManagerUrn(), "urn:publicid:IDN+rci-hn.exogeni.net+authority+cm"))
                sitename = "exogeni.net:rcivmsite";
            if (Objects.equals(auth.getDefaultComponentManagerUrn(), "urn:publicid:IDN+uh-hn.exogeni.net+authority+cm"))
                sitename = "exogeni.net:uhvmsite";
            if (Objects.equals(auth.getDefaultComponentManagerUrn(), "urn:publicid:IDN+uva-nl-hn.exogeni.net+authority+cm"))
                sitename = "exogeni.net:uvanlvmsite";
            if (Objects.equals(auth.getDefaultComponentManagerUrn(), "urn:publicid:IDN+ufl-hn.exogeni.net+authority+cm"))
                sitename = "exogeni.net:uflvmsite";
            if (Objects.equals(auth.getDefaultComponentManagerUrn(), "urn:publicid:IDN+osf-hn.exogeni.net+authority+cm"))
                sitename = "exogeni.net:osfvmsite";
            //note, there is also uncvmsite: University NC (but URN and URL not known)


            if (sitename != null)
                return "urn:publicid:IDN+" + sitename + "+authority+cm";
        }
        return auth.getDefaultComponentManagerUrn();
    }

    public static boolean getExclusiveForSite(Server auth) {
        String sliverType = getSliverTypeForSite(auth);
        if (Objects.equals(sliverType, "raw-pc"))
            return true;
        else
            return false;
    }

    public static String getSliverTypeForSite(Server auth) {
        if (auth.isServerType("emulab"))
            return "raw-pc";
        if (auth.isServerType("instageni"))
            return "emulab-xen";
        if (auth.isServerType("planetlab"))
            return "plab-vserver";
        if (auth.getServerType() != null && (auth.getServerType().equals("orca") || auth.getServerType().equals("exogeni")))
            return "xo.small";
        if (auth.getUrnTld().contains("exogeni") || auth.getUrnTld().contains("vmsite") || auth.getUrnTld().contains("orca"))
            return "xo.small";

        return null;
    }

    /**
     * returns an RSpec for one node, for the given authority.
     */
    public String getOneNodeRequestRSpec(Server auth, GeniUrn sliceUrn) {
        return getOneNodeRequestRSpec(auth, sliceUrn, null/*fixedNodeUrn*/, null, null);
    }

    public String getOneNodeRequestRSpec(Server auth, GeniUrn sliceUrn, @Nullable GeniUrn fixedNodeUrn, Boolean exclusive, String sliverType) {
        String componentId = "";
        if (fixedNodeUrn != null)
            componentId = " component_id=\"" + fixedNodeUrn + "\" ";

        if (Objects.equals(auth.getDefaultComponentManagerAsGeniUrn(), GeniUrn.parse("urn:publicid:IDN+fiteagle+authority+am"))) {
            Random rand = new Random(System.currentTimeMillis());
            return "<rspec type=\"request\" xsi:schemaLocation=\"http://www.geni.net/resources/rspec/3 http://www.geni.net/resources/rspec/3/ad.xsd http://fiteagle.org/rspec/ext/1 http://fiteagle.org/rspec/ext/1\" xmlns:ns2=\"http://fiteagle.org/rspec/ext/1\" xmlns=\"http://www.geni.net/resources/rspec/3\" xmlns:ns3=\"http://fiteagle.org/rspec/ext/1/openstackVMResource\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
                    "<ns3:openstackResource resourceId=\"42ad1e74-c9cd-4fe0-9d18-522b7f8bede1\">\n" +
                    "<ns3:vmToInstantiate imageId=\"d6521c1b-4407-454a-981f-b5d4d4d5fcae\" flavorId=\"3\" vmName=\"randomName" + rand.nextInt() + "\" keyPairName=\"randomKeyPair" + rand.nextInt() + "\"/>\n" +
                    "</ns3:openstackResource>\n" +
                    "</rspec>";
        }
        if (Objects.equals(auth.getDefaultComponentManagerAsGeniUrn(), GeniUrn.parse("urn:publicid:IDN+clab+authority+cm"))) {
            return "<rspec type=\"request\" xsi:schemaLocation=\"http://www.geni.net/resources/rspec/3 http://www.geni.net/resources/rspec/3/request.xsd \" " +
                    "xmlns:client=\"http://www.protogeni.net/resources/rspec/ext/client/1\" " +
                    "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
                    "xmlns=\"http://www.geni.net/resources/rspec/3\">\n" +
                    "     <node client_id=\"myNodeId\" component_manager_id=\"urn:publicid:IDN+clab+authority+cm\" " + componentId + " exclusive=\"false\">\n" +
                    "         <sliver_type name=\"RD_sliver\"/>\n" +
                    "     </node>\n" +
                    "</rspec>";
        }
        if (auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority().startsWith("omf:nitos")
                || (TextUtil.objectToBoolean(test.getTestConfig().getProperty("use_nitos_lease", "false")) == Boolean.TRUE)
                ) {
            Random rand = new Random(System.currentTimeMillis());
            long leaseId = Math.abs(rand.nextInt());
            Date lease_start_date = new Date();
            Date lease_end_date = new Date(lease_start_date.getTime() + (2 * 60 * 60 * 1000L)); //2 hour lease

            String usedNodeCompman = /*auth.getDefaultComponentManagerAsGeniUrn().getTopLevelAuthority()*/
                    "omf:nitos.outdoor";
            String usedNodeName = "node001";
            if (fixedNodeUrn != null) {
                usedNodeCompman = fixedNodeUrn.getEncodedTopLevelAuthority();
                usedNodeName = fixedNodeUrn.getEncodedResourceName();
            }

            componentId = " component_id=\"urn:publicid:IDN+" + usedNodeCompman + "+node+" + usedNodeName + "\" ";

            return "<?xml version=\"1.0\"?>\n" +
                    "<rspec type=\"request\" " +
                    "       xmlns=\"http://www.geni.net/resources/rspec/3\" " +
                    "       xmlns:ol=\"http://nitlab.inf.uth.gr/schema/sfa/rspec/1\" " +
                    "       xmlns:omf=\"http://schema.mytestbed.net/sfa/rspec/1\" " +
                    "       xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
                    "       xsi:schemaLocation=\"http://www.geni.net/resources/rspec/3 http://www.geni.net/resources/rspec/3/request.xsd http://nitlab.inf.uth.gr/schema/sfa/rspec/1 http://nitlab.inf.uth.gr/schema/sfa/rspec/1/request-reservation.xsd\">\n" +
                    "  <ol:lease client_id=\"l" + leaseId + "\" " +
                    "            valid_from=\"" + RFC3339Util.dateToRFC3339String(lease_start_date) + "\" " +
                    "            valid_until=\"" + RFC3339Util.dateToRFC3339String(lease_end_date) + "\"/>\n" +
                    "  <node " + componentId + " " +
                    "        component_manager_id=\"" + auth.getDefaultComponentManagerAsGeniUrn() + "\" " +
                    "        component_name=\"" + usedNodeName + "\" " +
                    "        exclusive=\"true\" " +
                    "        client_id=\"my_node\">\n" +
//                    "    <interface component_id=\"urn:publicid:IDN+" + usedNodeCompman + "+interface+control\" " +
//                    "               component_name=\"control\">\n" +
//                    "      <ip address=\"10.0.0.1\"/>\n" +
//                    "    </interface>\n" +
                    "    <ol:lease_ref id_ref=\"l" + leaseId + "\"/>\n" +
                    "  </node>\n" +
                    "</rspec>";
        }
        if (Objects.equals(auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority(), "omf:netmode")) {
            Random rand = new Random(System.currentTimeMillis());
            long leaseId = Math.abs(rand.nextInt());
            Date lease_start_date = new Date();
            Date lease_end_date = new Date(lease_start_date.getTime() + (2 * 60 * 60 * 1000L)); //2 hour lease

            String componentName;
            if (fixedNodeUrn == null) {
                componentId =
                        " component_id=\"urn:publicid:IDN+" + auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority() + "+node+omf.netmode.node1\" ";
                componentName = "omf.netmode.node1";
            } else {
                componentName = fixedNodeUrn.getEncodedResourceName();
            }

            return "<?xml version=\"1.0\"?>\n" +
                    "<rspec type=\"request\" " +
                    "       xmlns=\"http://www.geni.net/resources/rspec/3\" " +
                    "       xmlns:ol=\"http://nitlab.inf.uth.gr/schema/sfa/rspec/1\" " +
                    "       xmlns:omf=\"http://schema.mytestbed.net/sfa/rspec/1\" " +
                    "       xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
                    "       xsi:schemaLocation=\"http://www.geni.net/resources/rspec/3 http://www.geni.net/resources/rspec/3/request.xsd http://nitlab.inf.uth.gr/schema/sfa/rspec/1 http://nitlab.inf.uth.gr/schema/sfa/rspec/1/request-reservation.xsd\">\n" +
                    "  <ol:lease client_id=\"l" + leaseId + "\" " +
                    "            valid_from=\"" + RFC3339Util.dateToRFC3339String(lease_start_date) + "\" " +
                    "            valid_until=\"" + RFC3339Util.dateToRFC3339String(lease_end_date) + "\"/>\n" +
                    "  <node " + componentId + " " +
                    "        component_manager_id=\"" + auth.getDefaultComponentManagerAsGeniUrn() + "\" " +
                    "        component_name=\"" + componentName + "\" " +
                    "        exclusive=\"true\" " +
                    "        client_id=\"my_node\">\n" +
//                    "    <interface component_id=\"urn:publicid:IDN+" + auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority() + "+interface+control\" " +
//                    "               component_name=\"control\">\n" +
//                    "      <ip address=\"10.0.0.1\"/>\n" +
//                    "    </interface>\n" +
                    "    <ol:lease_ref id_ref=\"l" + leaseId + "\"/>\n" +
                    "  </node>\n" +
                    "</rspec>";
        }
        if (Objects.equals(auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority(), "omf")) { //TODO fix this urn and rspec
            Random rand = new Random(System.currentTimeMillis());
            Date lease_start_date = new Date();
            long duration_unknownunit = 6;
            Date lease_end_date = new Date(lease_start_date.getTime() + (duration_unknownunit * 60 * 60 * 1000L)); //?
            return "<?xml version=\"1.0\"?>\n" +
                    "<RSpec type=\"NITOS\" expires=\"2013-10-31T19:30:08Z\" generated=\"2013-10-31T18:30:08Z\">\n" +
                    "  <network name=\"omf\">\n" +
                    "    <node component_manager_id=\"urn:publicid:IDN+omf+authority+cm\" " +
                    "          component_id=\"urn:publicid:IDN+" + auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority() + "+node+node001\"" +
                    "          component_name=\"node001\" " +
                    "          site_id=\"urn:publicid:IDN+" + auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority() + "+authority+sa\">\n" +
                    "      <hostname>node001</hostname>\n" +
                    "      <location country=\"unknown\" longitude=\"39.360839\" latitude=\"22.949989\"/>\n" +
                    "      <position_3d x=\"1\" y=\"1\" z=\"5\"/>\n" +
                    "      <exclusive>TRUE</exclusive>\n" +
                    "      <gateway>nitlab.inf.uth.gr</gateway>\n" +
                    "      <granularity>1800</granularity>\n" +
                    "      <hardware_type>orbit</hardware_type>\n" +
                    "      <sliver/>\n" +
                    "    </node>\n" +
                    "    <lease slice_id=\"" + sliceUrn + "\" start_time=\"" + lease_start_date.getTime() + "\" duration=\"" + duration_unknownunit + "\">\n" +
                    "      <node component_id=\"urn:publicid:IDN+" + auth.getDefaultComponentManagerAsGeniUrn().getEncodedTopLevelAuthority() + "+node+node001\"/>\n" +
                    "      </lease>\n" +
                    "  </network>\n" +
                    "</RSpec>";
        }


        if (auth.isServerType("emulab")) {
            if (exclusive == null)
                exclusive = true;
            if (sliverType == null)
                sliverType = "raw-pc";

            return "<rspec type=\"request\" xsi:schemaLocation=\"http://www.geni.net/resources/rspec/3 http://www.geni.net/resources/rspec/3/request.xsd \" xmlns:client=\"http://www.protogeni.net/resources/rspec/ext/client/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.geni.net/resources/rspec/3\">\n" +
                    "  <node client_id=\"PC\" component_manager_id=\"" + auth.getDefaultComponentManagerUrn() + "\" " + componentId + " exclusive=\"" + exclusive + "\">\n" +
                    "    <sliver_type name=\"" + sliverType + "\"/>\n" +
                    "  </node>\n" +
                    "</rspec>\n";
        }
        if (auth.isServerType("instageni")) {
            if (exclusive == null)
                exclusive = false;
            if (sliverType == null)
                sliverType = "emulab-xen";

            return "<rspec type=\"request\" xsi:schemaLocation=\"http://www.geni.net/resources/rspec/3 http://www.geni.net/resources/rspec/3/request.xsd \" xmlns:client=\"http://www.protogeni.net/resources/rspec/ext/client/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.geni.net/resources/rspec/3\">\n" +
                    "  <node client_id=\"PC\" component_manager_id=\"" + auth.getDefaultComponentManagerUrn() + "\" " + componentId + " exclusive=\"" + exclusive + "\">\n" +
                    "    <sliver_type name=\"" + sliverType + "\"/>\n" +
                    "  </node>\n" +
                    "</rspec>\n";
        }
        if (auth.isServerType("planetlab")) {
            if (fixedNodeUrn == null)
                componentId = " component_id=\"urn:publicid:IDN+ple:unistra+node+planetlab1.u-strasbg.fr\" ";
            return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" +
                    "   <rspec type=\"request\" " +
                    " xmlns=\"http://www.geni.net/resources/rspec/3\" >\n" +
                    "       <node client_id=\"node0\" " +
                    componentId +
                    " component_manager_id=\"urn:publicid:IDN+ple:ibbtple+authority+cm\" " +
//                    " component_name=\"planetlab1.u-strasbg.fr\" " +
                    " exclusive=\"false\">\n" +
                    "           <sliver/>\n" +
                    "           <sliver_type name=\"plab-vserver\"/>\n" +
//                    "           <location country=\"unknown\" longitude=\"7.73833\" latitude=\"48.5237\"/>\n" +
                    "           <hardware_type name=\"plab-pc\"/>\n" +
                    "           <hardware_type name=\"pc\"/>\n" +
                    "       </node>\n" +
                    "   </rspec>";
            //OLD
//            return "<rspec type=\"request\" xsi:schemaLocation=\"http://www.geni.net/resources/rspec/3 http://www.geni.net/resources/rspec/3/request.xsd \" xmlns:client=\"http://www.protogeni.net/resources/rspec/ext/client/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.geni.net/resources/rspec/3\">\n" +
//                    "    <node component_id=\"urn:publicid:IDN+ple:unistra+node+planetlab1.u-strasbg.fr\" component_manager_id=\"urn:publicid:IDN+ple+authority+cm\" component_name=\"planetlab1.u-strasbg.fr\" exclusive=\"false\">\n" +
//                    "           <hardware_type name=\"plab-pc\"/>\n" +
//                    "           <hardware_type name=\"pc\"/>\n" +
//                    "           <location country=\"unknown\" latitude=\"48.5237\" longitude=\"7.73833\"/>\n" +
//                    "           <sliver_type name=\"plab-vserver\">\n" +
//                    "           </sliver_type>\n" +
//                    "       </node>\n" +
//                    "   </rspec>";
        }
        if (auth.getServerType() != null && (auth.getServerType().equals("orca") || auth.getServerType().equals("exogeni"))) {
            //see https://geni-orca.renci.org/trac/wiki/orca-and-rspec
            String componentManagerId = getComponentManagerIdForSite(auth);

            if (exclusive == null)
                exclusive = false;
            if (sliverType == null)
                sliverType = "xo.small";//1 core node (smallest available)

            return "<rspec type=\"request\" xsi:schemaLocation=\"http://www.geni.net/resources/rspec/3 http://www.geni.net/resources/rspec/3/request.xsd \" xmlns:client=\"http://www.protogeni.net/resources/rspec/ext/client/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.geni.net/resources/rspec/3\">\n" +
                    //                    "  <node client_id=\"PC\" component_manager_id=\""+auth.getDefaultComponentManagerUrn()+"\" exclusive=\"false\">\n" +
                    "  <node client_id=\"PC\" component_manager_id=\"" + componentManagerId + "\" " + componentId + " exclusive=\"" + exclusive + "\">\n" +
                    //                    "    <sliver_type name=\"ExoGENI-M4\"/>\n" + //bare metal node
                    "    <sliver_type name=\"" + sliverType + "\"/>\n" +
                    "  </node>\n" +
                    "</rspec>\n";
        }

        if (exclusive == null)
            exclusive = false;
        //otherwise, request a non exclusive node without specifying sliver type
        return "<rspec type=\"request\" xsi:schemaLocation=\"http://www.geni.net/resources/rspec/3 http://www.geni.net/resources/rspec/3/request.xsd \" xmlns:client=\"http://www.protogeni.net/resources/rspec/ext/client/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.geni.net/resources/rspec/3\">\n" +
                "  <node client_id=\"PC\" component_manager_id=\"" + auth.getDefaultComponentManagerUrn() + "\" " + componentId + " exclusive=\"" + exclusive + "\">\n" +
                (sliverType == null ? "" : "<sliver_type name=\"" + sliverType + "\"/>\n") +
                "  </node>\n" +
                "</rspec>\n";
    }

    public static boolean isEmptyRspec(ApiTest test, String rspec) {
        if (rspec == null || rspec.trim().isEmpty())
            return true;

        BasicStringRspec basicStringRspec = new BasicStringRspec(rspec);
        if (!basicStringRspec.isWellFormed())
            throw new RuntimeException("Not an rspec (not even well formed XML): " + rspec);

        if (!basicStringRspec.isValidRspec())
            throw new RuntimeException("Not an rspec (it is well formed XML, but not an RSpec): " + rspec);

        return basicStringRspec.getBasicNodeInfo().isEmpty();
    }

    /**
     * returns nodecount
     */
    public static int testValidGeni3ManifestRspec(ApiTest test, String rspec) {
        if (rspec == null) return -1;
        test.assertNotNull(rspec, "Rspec is null");

        //quick HACK to allow the planetlab Europe rspec manifest
        // FEDIBBTDEV-113, bvermeul, copied this nice stuff from suite/rspec-edit/src/main/java/be/iminds/ilabt/jfed/ui/rspeceditor/model/Rspec.java
        if (rspec.contains("<RSpec")) {
            test.note("We received an SFA RSpec instead of a GENI RSpec v3, but we'll try to work around.");
            //logger.debug("HACK: rewriting received planetlab manifest XML to make it parsable xml");
            rspec =
                    rspec.replace("<RSpec type=\"SFA\"", "<rspec xmlns=\"http://www.geni.net/resources/rspec/3\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" type=\"manifest\"");
            rspec =
                    rspec.replace("<RSpec ", "<rspec xmlns=\"http://www.geni.net/resources/rspec/3\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" type=\"manifest\"");
            rspec = rspec.replace("</RSpec>", "</rspec>");
            rspec = rspec.replace("<network name=\"ple\">", "");
            rspec = rspec.replace("</network>", "");
            //logger.debug("HACK: rewriten xml: "+rspec);
        }


        BasicStringRspec basicStringRspec = new BasicStringRspec(rspec);
        test.assertTrue(basicStringRspec.isWellFormed(), "Not an rspec (not even well formed XML): " + rspec);
        test.assertTrue(basicStringRspec.isValidRspec(), "Not an rspec (it is well formed XML, but not an RSpec): " + rspec);

        String type = basicStringRspec.getRspecType();
        test.assertNotNull(type, "Received manifest RSpec does not specify a type: " + rspec);
        test.assertEquals(type, "manifest", "Received manifest RSpec is not a manifest: " + rspec);

        return basicStringRspec.getBasicNodeInfo().size();
    }

    /**
     * returns nodecount
     */
    public static int testValidGeni3AdvertisementRspec(ApiTest test, String rspec) {
        if (rspec == null) return -1;
        test.assertNotNull(rspec, "Rspec is null");

        BasicStringRspec basicStringRspec = new BasicStringRspec(rspec);
        test.assertTrue(basicStringRspec.isWellFormed(), "Not an rspec (not even well formed XML): " + rspec);
        test.assertTrue(basicStringRspec.isValidRspec(), "Not an rspec (it is well formed XML, but not an RSpec): " + rspec);

        String type = basicStringRspec.getRspecType();
        test.assertNotNull(type, "Received advertisement RSpec does not specify a type: " + rspec);
        test.assertEquals(type, "advertisement", "Received advertisement RSpec is not a manifest: " + rspec);

        return basicStringRspec.getBasicNodeInfo().size();
    }

    public static String createSliceName(ApiTest test, Server userAuthority, List<GeniUrn> testedAuthorityCms, String prefix, int maxNodeNameLength, SpeaksForConfig speaksForConfig) {
        int maxSliceNameLength = 19; //Geni max length

        int maxTestedAuthorityCmLen = testedAuthorityCms.get(0).getEncodedTopLevelAuthority().length();
        String longestCmUrn = null;
        for (GeniUrn u : testedAuthorityCms)
            if (u.getEncodedTopLevelAuthority().length() >= maxTestedAuthorityCmLen) {
                maxTestedAuthorityCmLen = u.getEncodedTopLevelAuthority().length();
                longestCmUrn = u.getEncodedTopLevelAuthority();
            }

        String maxUserAuthName = userAuthority.getUrnTld();
        int maxUserAuthLen = userAuthority.getUrnTld().length();
        //handle speaksFor case
        if (speaksForConfig != null && speaksForConfig.getUser2Urn() != null && speaksForConfig.getUser2Urn().getEncodedTopLevelAuthority().length() > maxUserAuthLen) {
            maxUserAuthLen = speaksForConfig.getUser2Urn().getEncodedTopLevelAuthority().length();
            maxUserAuthName = speaksForConfig.getUser2Urn().getEncodedTopLevelAuthority();
        }

        test.note("The longest CM urn top level authority is: \"" + longestCmUrn + "\" (" + maxTestedAuthorityCmLen + " chars)");
        test.note("The user authority of max length is: \"" + maxUserAuthName + "\" (" + maxUserAuthLen + " chars)");
        test.note("The maximum node name length is: " + maxNodeNameLength);

        //planetlab sfa max length
        int authLen = userAuthority.getUrnTld().replaceAll("\\.", "").length();
        int planetlab_l = 32 - authLen;
        //protogeni error example:            Hostname > 63 characters: PC.sfbD481228.wall2-ilabt-iminds-be.instageni.metrodatacenter.com
        //protogeni max length of FQDN = 64 characters (unix limit):  PC.st1383985554.wall2-ilabt-iminds-be.instageni.rnet.missouri.edu
        final int dotcount = 3;
        final int nodeNameLen = maxNodeNameLength;  // note: auto generated node names are max 3 characters
        int protogeni_l = 63 - maxTestedAuthorityCmLen - maxUserAuthLen - nodeNameLen - dotcount;
        int l = 0;
        if (protogeni_l < planetlab_l) {
            l = protogeni_l;
        } else {
            l = planetlab_l;
        }
        if (l < maxSliceNameLength) {
            maxSliceNameLength = l;
            test.note("For " + userAuthority.getUrnTld() + " (" + authLen + " non-dot chars), the maximum slice name length (taking into account planetlab and protogeni limits) is " + maxSliceNameLength);
        } else
            test.note("Slice name length is " + maxSliceNameLength);

//        long uniqueNumber = System.currentTimeMillis() / 1000; //unique each second
//        // uniqueNumber /= 10; //unique each 10 seconds
//        String uniqueNumberString = uniqueNumber + "";
//        int uniqueNumberMaxLen = maxSliceNameLength - prefix.length();
//        uniqueNumberMaxLen -= 3; //for uniqueText
//        if (uniqueNumberString.length() > uniqueNumberMaxLen) {
//            //drop from front of string, not from back
//            uniqueNumberString = uniqueNumberString.substring(uniqueNumberString.length() - uniqueNumberMaxLen);
//            test.assertEquals(uniqueNumberString.length(), uniqueNumberMaxLen, "bug in test: uniqueNumberString.length()=" + uniqueNumberString.length() + " !=  uniqueNumberMaxLen=" + uniqueNumberMaxLen);
//
//            test.note("Slice unique number is " + uniqueNumber + " after making it shorter it, it is " + uniqueNumberString);
//        }

        //just use RANDOM text.
        int uniqueTextLen = maxSliceNameLength - prefix.length();

        if (uniqueTextLen < 2) {
            uniqueTextLen = 2;
            test.note("The calculated slice name length is " + maxSliceNameLength + " which is too short. It will be increased to " + (prefix.length() + uniqueTextLen) + " create a usable slice name for this test.\n" +
                    "This slicename consists of a prefix (\"" + prefix + "\" = " + prefix.length() + " chars) and a unique suffix (" + uniqueTextLen + " RANDOM chars)");
        }

        String uniqueTextHelper = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        String uniqueText = "";
        for (int i = 0; i < uniqueTextLen; i++) {
            int pos = RANDOM.nextInt(uniqueTextHelper.length());
            uniqueText += uniqueTextHelper.charAt(pos);
        }

//        String longSliceName = prefix + uniqueText + uniqueNumberString;
        String longSliceName = prefix + uniqueText;
        return longSliceName;
    }

    public static void addOptionsForNodeLoginSshProxy(List<String> res) {
        //proxy for ssh connections
        res.add("loginSshProxyMode"); //valid option: off (default), manual(use preferences below), user(automatically use proxy of user authority)

        res.add("loginSshProxyHostName");
        res.add("loginSshProxyHostKey");
        res.add("loginSshProxyHostPort");
        res.add("loginSshProxyUsername"); //default: same username as logged in user (same as "$USER"). Some limited variable substitution is done:
        // - $NODE: username for target node
        // - $USER: logged in user name (last part of user URN)
        // - $SLICE: slice name (last part of slice URN)
        res.add("loginSshProxyKeySource"); //valid choices:
        // - user (take SSH keys from user login cert and key)
        // - node (use the same SSH keys as the one to login to the actual target node, these are typically generated by NodeLoginTester, unless fixed_ssh_public_key_file says otherwise)
        // - file: take from file name specified in sshProxyPrivateKeyFile and sshProxyPublicKeyFile option (not yet implemented)
    }

    public static void addOptionsForConnectionSshProxy(List<String> res) {
        res.add("sfaSshProxyMode"); //valid option: off (default), manual(use preferences below), user(automatically use proxy of user authority)

        //proxy for sfa connections
        res.add("sfaSshProxyHostName");
        res.add("sfaSshProxyHostKey");
        res.add("sfaSshProxyHostPort");
        res.add("sfaSshProxyUsername"); //default: same username as logged in user (same as "$USER"). Some limited variable substitution is done:
        // - $USER: logged in user name (last part of user URN)
        res.add("sfaSshProxyKeySource"); //valid choices:
        // - user (take SSH keys from user login cert and key)
        // - file: take from file name specified in sshProxyPrivateKeyFile and sshProxyPublicKeyFile option (not yet implemented)

    }

    public static void addOptionsForRspecTypeVersion(List<String> res) {
        res.add("rspecTypeName"); //default: "geni"
        res.add("rspecTypeVersion"); //default: "3"
    }

    public String getRspecTypeName(Properties testConfig) {
        String res = TextUtil.objectToStringOrNull(testConfig.get("rspecTypeName"), true);
        if (res == null)
            return "geni";
        return res;
    }

    public String getRspecTypeVersion(Properties testConfig) {
        String res = TextUtil.objectToStringOrNull(testConfig.get("rspecTypeVersion"), true);
        if (res == null)
            return "3";
        return res;
    }

    public boolean isRspecTypeGeni3(Properties testConfig) {
        String name = TextUtil.objectToStringOrNull(testConfig.get("rspecTypeName"), true);
        String ver = TextUtil.objectToStringOrNull(testConfig.get("rspecTypeVersion"), true);
        return (name == null || name.equalsIgnoreCase("geni")) && (ver == null || ver.equals("3"));
    }

    /**
     * returns true if A is preferred over B
     */
    public static boolean preferSliverType(String sliverTypeNameA, String sliverTypeNameB) {
        if (sliverTypeNameB == null) return true;
        int scoreA = getSliverTypeNameScore(sliverTypeNameA);
        int scoreB = getSliverTypeNameScore(sliverTypeNameB);
        return scoreA > scoreB;
    }

    /**
     * returns a score for the sliver type name. Higher scores are preferred.
     */
    public static int getSliverTypeNameScore(String sliverTypeName) {
        if (sliverTypeName == null) return 0;
        if (sliverTypeName.equalsIgnoreCase("emulab-openvz")) return 100;
        if (sliverTypeName.equalsIgnoreCase("emulab-xen")) return 98;
        if (sliverTypeName.equalsIgnoreCase("plab-vserver")) return 99;

        if (sliverTypeName.equalsIgnoreCase("raw-pc")) return 90;

        //exogeni
        if (sliverTypeName.equalsIgnoreCase("xo.small")) return 80;
        if (sliverTypeName.equalsIgnoreCase("xo.tiny")) return 81;
        if (sliverTypeName.startsWith("xo.")) return 79;

        //various
        if (sliverTypeName.contains("xen")) return 63;
        if (sliverTypeName.contains("openvz")) return 62;
        if (sliverTypeName.contains("virt")) return 61;
        if (sliverTypeName.contains("vz")) return 60;

        if (sliverTypeName.contains("raw")) return 11;
        if (sliverTypeName.contains("pc")) return 10;

        return 1;
    }

    public List<AnyCredential> getUser2Credentials() {
        assert test != null;
        assert secondUserConfig.getUser() != null;

        if (user2Credentials != null)
            return user2Credentials;

        try {
            user2Credentials =
                    userAndSliceApiWrapperUser2.getUserCredentials(test.getLogger(), GeniUrn.parse(secondUserConfig.getUser().getUserUrnString()));
        } catch (JFedException e) {
            throw new RuntimeException(e);
        }
        assert user2Credentials != null;
        assert !user2Credentials.isEmpty();
        return user2Credentials;
    }

    private List<AnyCredential> getUserCredentialsList() {
        assert test != null;
        assert userAuthority != null;

        if (userCredentials != null && !userCredentials.isEmpty())
            return userCredentials;

        try {
            if (testCredentialType == TestCredentialType.SPEAKSFOR && speaksForConfig.isSARequestMethodSpeaksFor()) {
                userAndSliceApiWrapperUser1.setSpeaksFor(speaksForConfig.getSpeaksForCredentials(), speaksForConfig.getUser2Urn());
                if (!speaksForConfig.isAutomaticSpeaksForOptionNames())
                    userAndSliceApiWrapperUser1.setExtraOptionsForCallsWithCredential(addCredentialExtraOptions(null));
            }

            if (testCredentialType == TestCredentialType.REGULAR ||
                    (testCredentialType == TestCredentialType.SPEAKSFOR && speaksForConfig.isSARequestMethodSpeaksFor())) {
                GeniUrn userUrn;
                if (testCredentialType == TestCredentialType.SPEAKSFOR) {
                    assert speaksForConfig.isSARequestMethodSpeaksFor();
                    userUrn = speaksForConfig.getUser2Urn();
                } else
                    userUrn = test.getUser().getUserUrn();

                userCredentials =
                        userAndSliceApiWrapperUser1.getUserCredentials(test.getLogger(), userUrn);
            } else {
                userCredentials =
                        userAndSliceApiWrapperUser2.getUserCredentials(test.getLogger(), secondUserConfig.getUser().getUserUrn());
            }
        } catch (JFedException e) {
            throw new RuntimeException(e);
        }
        assert userCredentials != null;
        assert !userCredentials.isEmpty();
        return userCredentials;
    }

    public List<AnyCredential> getUserCredentialListForAM() {
        if (testCredentialType == TestCredentialType.REGULAR)
            return getUserCredentialsList();

        if (testCredentialType == TestCredentialType.SPEAKSFOR) {
            List<AnyCredential> res = new ArrayList<>();
            assert speaksForConfig.getSpeaksForCredentials() != null;
            speaksForConfig.getSpeaksForCredentials().stream()
                    .filter(s4cred -> !res.contains(s4cred))
                    .forEach(s4cred -> {
                        test.note("Implementation bug (has been auto-fixed): speaksfor credential was not added automatically to user credential list");
                        res.add(s4cred);
                    });
            res.addAll(getUserCredentialsList());
            return res;
        }

        if (testCredentialType == TestCredentialType.DELEGATION)
            return Collections.singletonList(getDelegatedUserCredential());

        throw new RuntimeException("unsupported credential type: " + testCredentialType);
    }

    public List<AnyCredential> getSliceCredentialListForAM(SliceInfo sliceInfo) {
        if (testCredentialType == TestCredentialType.REGULAR)
            return sliceInfo.credentials;

        if (testCredentialType == TestCredentialType.SPEAKSFOR) {
            //if (speaksForConfig.isSARequestMethodLogin()) {
            List<AnyCredential> res = new ArrayList<>();

            List<AnyCredential> speaksForCredentials = speaksForConfig.getSpeaksForCredentials();
            if (!sliceInfo.credentials.containsAll(speaksForCredentials)) {
                //should not be needed, added automatically by the wrapper
                assert speaksForCredentials != null && !speaksForCredentials.isEmpty();
                res.addAll(speaksForCredentials);
                test.note("Implementation bug (has been auto-fixed): The wrapper is not automatically adding the speaksFor credential to the list " +
                        "of slice credentials");
            }

            res.addAll(sliceInfo.credentials);
            return res;
        }

        if (testCredentialType == TestCredentialType.DELEGATION)
            return Collections.singletonList(sliceInfo.getDelegatedCredential(test.getUser(), secondUserConfig.getUser()));

        throw new RuntimeException("unsupported credential type: " + testCredentialType);
//        assert testCredentialType == TestCredentialType.SPEAKSFOR;
//        List<AnyCredential> res = new ArrayList<AnyCredential>();
//        res.addAll(sliceInfo.credentials);
//        res.add(getSpeaksForUserCredential());
//        return res;
    }

    /**
     * add speaksfor option if needed
     *
     * @param extraOptions does not change original extraOptions, makes a copy. allowed to be null.
     */
    public Map<String, Object> addCredentialExtraOptions(Map<String, Object> extraOptions) {
        if (testCredentialType != TestCredentialType.SPEAKSFOR)
            return extraOptions;


        assert (!speaksForConfig.isAutomaticSpeaksForOptionNames() && speaksForConfig.getSpeaksForOptionNames().isEmpty()) || speaksForConfig.getUser2Urn() != null;

        if (speaksForConfig.isAutomaticSpeaksForOptionNames() || speaksForConfig.getSpeaksForOptionNames().isEmpty())
            return extraOptions;

        Map<String,Object> res = new HashMap<>();
        if (extraOptions != null) res.putAll(extraOptions);

        for (String speaksForOptionName : speaksForConfig.getSpeaksForOptionNames())
            res.put(speaksForOptionName, speaksForConfig.getUser2Urn().getValue());

        return res;
    }

    public void setupProxyForConnectionPool(Properties testConfig) {
        String mode;
        if (testConfig.get("sfaSshProxyMode") != null) {
            mode = testConfig.getProperty("sfaSshProxyMode");
            if (!Objects.equals(mode, "manual") && !Objects.equals(mode, "user"))
                return;
        } else
            return;

        if (Objects.equals(mode, "manual") && testConfig.get("sfaSshProxyHostName") != null) {
            String sfaSshProxyHostName = (String) testConfig.get("sfaSshProxyHostName");
            int sfaSshProxyHostPort =
                    Integer.parseInt(testConfig.get("sfaSshProxyHostPort") + ""); //TODO: does Properties store an Integer or a String? (this code works in both cases)
            String sfaSshProxyHostKey = (String) testConfig.get("sfaSshProxyHostKey");

            String username;
            boolean limitName;
            if (testConfig.get("sfaSshProxyUsername") != null) {
                String sfaSshProxyUsername = (String) testConfig.get("sfaSshProxyUsername");
                String loggedInUserName = test.getUser().getUserUrn().getEncodedResourceName();

                username = sfaSshProxyUsername;
                limitName = username.contains("$USER");
                username = username.replace("$USER", loggedInUserName);
            } else {
                limitName = true;
                username = test.getUser().getUserUrn().getEncodedResourceName();
            }

            String sfaSshProxyKeySource = (String) testConfig.get("sfaSshProxyKeySource");
            SshKeyInfo sshKeyInfo = null;
            if (sfaSshProxyKeySource.equalsIgnoreCase("user")) {
                test.note("Using user private key as proxy private key.");
                sshKeyInfo = SshKeyInfoFactory.createGeniUserSshKeyInfo(test.getUser());
            }
            if (sfaSshProxyKeySource.equalsIgnoreCase("file")) {
                test.warn("Configuration warning: Test option sfaSshProxyKeySource=\"" + sfaSshProxyKeySource + "\" is not implemented yet. Cannot continue, will do test WITHOUT proxy.");
            }

            if (sshKeyInfo == null) {
                test.warn("Configuration error: Test option sfaSshProxyKeySource=\"" + sfaSshProxyKeySource + "\" is not known. Cannot continue, will do test WITHOUT proxy.");
            } else {
                JFedConnection.SshProxyInfo sfaSshProxyInfo = new JFedConnection.SshProxyInfo(
                        sfaSshProxyHostName,
                        sfaSshProxyHostPort,
                        username,
                        sshKeyInfo,
                        sfaSshProxyHostKey,
                        limitName);
                test.note("Using SSH proxy for SSH login test: " + sfaSshProxyInfo);
                ((SfaConnectionPool) test.getConnectionProvider()).setDefaultProxy(sfaSshProxyInfo);
            }
            return;
        }

        if (Objects.equals(mode, "user")) {
            GeniUser user = test.getUser();
            Server userServer = user.getUserAuthorityServer();

            if (userServer.getTestbed().getProxies().isEmpty()) {
                test.warn("This logged in user's authority does not have a proxy that jFed can use.");
                return;
            }

            Proxy authProxyInfo = userServer.getTestbed().getProxies().get(0);

            VlanRange portRange = new VlanRange(authProxyInfo.getPortRange());
            Integer port = portRange.getFirst();

//            while (port != null && unreachableProxies.contains(new ProxyInfo(authProxyInfo.getHostname(), port))) {
//                port = portRange.getNext();
//            }
//            if (port == null) {
//                LOG.debug("Proxy at " + authProxyInfo.getHostname() + " has no reachable ports, skipping.");
//                continue;
//            }
//            LOG.info("SSH Proxy is activated, and there is a reachable SSH Proxy for user authority " + userAuth.getName() + ": " + authProxyInfo.getHostname() + ":" + port);

            SshKeyInfo userSshKeyInfo = SshKeyInfoFactory.createGeniUserSshKeyInfo(test.getUser());

            JFedConnection.ProxyInfo proxyInfo = new JFedConnection.SshProxyInfo(
                    authProxyInfo.getHostname(),
                    port,
                    user.getUserUrn().getEncodedResourceName(),
                    userSshKeyInfo,
                    authProxyInfo.getHostKey(),
                    true/*limitName*/);

            test.note("Using proxy for AM connections: " + proxyInfo);
            ((SfaConnectionPool) test.getConnectionProvider()).setDefaultProxy(proxyInfo);
            return;
        }
    }

    public void setupProxyForNodeLogin(Properties testConfig, NodeLoginTester nodeLoginTester, GeniUrn sliceUrn) {
        //ugly cut and paste code for backward compatibility. May be removed later!
        //    (this ignores loginSshProxyMode when sshProxyHostName property is found. This is the old property, and it is not advertised anymore, so only old configs still use this)
        if (testConfig.get("sshProxyHostName") != null) {
            boolean limitName;
            String sshProxyHostName = (String) testConfig.get("sshProxyHostName");
            int sshProxyHostPort = Integer.parseInt(testConfig.get("sshProxyHostPort") + "");
            String sshProxyHostKey = (String) testConfig.get("sshProxyHostKey");

            String username;
            if (testConfig.get("sshProxyUsername") != null) {
                String sshProxyUsername = (String) testConfig.get("sshProxyUsername");
                String loggedInUserName = test.getUser().getUserUrn().getEncodedResourceName();
                String sliceName = sliceUrn.getEncodedResourceName();
                String nodeUsername = nodeLoginTester.getSshUsername();

                username = sshProxyUsername;
                limitName = username.contains("$USER") || username.contains("$NODE") || username.contains("$SLICE");
                username = username.replace("$USER", loggedInUserName);
                username = username.replace("$NODE", nodeUsername);
                username = username.replace("$SLICE", sliceName);
            } else {
                limitName = true;
                username = test.getUser().getUserUrn().getEncodedResourceName();
            }

            String sshProxyKeySource = (String) testConfig.get("sshProxyKeySource");
            SshKeyInfo sshKeyInfo = null;
            if (sshProxyKeySource.equalsIgnoreCase("user")) {
                test.note("Using user private key as proxy private key.");
                sshKeyInfo = SshKeyInfoFactory.createGeniUserSshKeyInfo(test.getUser());
            }
            if (sshProxyKeySource.equalsIgnoreCase("node")) {
                test.note("Using node private key as proxy private key.");
                sshKeyInfo = nodeLoginTester.getSshKeyHelper().toSshKeyInfo();
            }
            if (sshProxyKeySource.equalsIgnoreCase("file")) {
                test.warn("Configuration warning: Test option sshProxyKeySource=\"" + sshProxyKeySource + "\" is not implemented yet. Cannot continue, will do test WITHOUT proxy.");
            }

            if (sshKeyInfo == null) {
                test.warn("Configuration error: Test option sshProxyKeySource=\"" + sshProxyKeySource + "\" is not unknown. Cannot continue, will do test WITHOUT proxy.");
            } else {
                JFedConnection.SshProxyInfo sshProxyInfo = new JFedConnection.SshProxyInfo(
                        sshProxyHostName,
                        sshProxyHostPort,
                        username,
                        sshKeyInfo,
                        sshProxyHostKey,
                        limitName);
                test.note("Using SSH proxy for SSH login test: " + sshProxyInfo);

                nodeLoginTester.setSshProxy(sshProxyInfo);
            }
        }

        //detect if manifest has proxy info
        if (nodeLoginTester.getManifestSshProxy() != null) {
            JFedConnection.SshProxyInfo manifestSshProxyInfo = nodeLoginTester.getManifestSshProxy();
            //fill in user keys to be used (nodeLoginTester does not know these so doesn't store these yet!)
            JFedConnection.SshProxyInfo sshProxyInfo = new JFedConnection.SshProxyInfo(
                    manifestSshProxyInfo.getHostname(),
                    manifestSshProxyInfo.getPort(),
                    manifestSshProxyInfo.getUsername(),
                    nodeLoginTester.getSshKeyHelper().toSshKeyInfo(),
                    null/*proxy host key*/,
                    false /*limitName is always false here: the manifest should already contain the correct name, we shouldn't ever need to modify it. */
            );
            nodeLoginTester.setSshProxy(sshProxyInfo);
            //this does not return, which means that "manual" and "user" will override the settings from the manifest!
        }

        //normal (new version) code:

        String mode;
        if (testConfig.get("loginSshProxyMode") != null) {
            mode = testConfig.getProperty("loginSshProxyMode");
            if (!Objects.equals(mode, "manual") && !Objects.equals(mode, "user"))
                return;
        } else
            return;

        if (Objects.equals(mode, "manual") && testConfig.get("loginSshProxyHostName") != null) {
            String loginSshProxyHostName = (String) testConfig.get("loginSshProxyHostName");
            int loginSshProxyHostPort =
                    Integer.parseInt(testConfig.get("loginSshProxyHostPort") + ""); //TODO: does Properties store an Integer or a String? (this code works in both cases)
            String loginSshProxyHostKey = (String) testConfig.get("loginSshProxyHostKey");

            boolean limitName;
            String username;
            if (testConfig.get("loginSshProxyUsername") != null) {
                String loginSshProxyUsername = (String) testConfig.get("loginSshProxyUsername");
                String loggedInUserName = test.getUser().getUserUrn().getEncodedResourceName();
                String sliceName = sliceUrn.getEncodedResourceName();
                String nodeUsername = nodeLoginTester.getSshUsername();

                username = loginSshProxyUsername;
                limitName = username.contains("$USER") || username.contains("$NODE") || username.contains("$SLICE");
                username = username.replace("$USER", loggedInUserName);
                username = username.replace("$NODE", nodeUsername);
                username = username.replace("$SLICE", sliceName);
            } else {
                limitName = true;
                username = test.getUser().getUserUrn().getEncodedResourceName();
            }

            String loginSshProxyKeySource = (String) testConfig.get("loginSshProxyKeySource");
            SshKeyInfo sshKeyInfo = null;
            if (loginSshProxyKeySource.equalsIgnoreCase("user")) {
                test.note("Using user private key as proxy private key.");
                sshKeyInfo = SshKeyInfoFactory.createGeniUserSshKeyInfo(test.getUser());
            }
            if (loginSshProxyKeySource.equalsIgnoreCase("node")) {
                test.note("Using node private key as proxy private key.");
                sshKeyInfo = nodeLoginTester.getSshKeyHelper().toSshKeyInfo();
            }
            if (loginSshProxyKeySource.equalsIgnoreCase("file")) {
                test.warn("Configuration warning: Test option loginSshProxyKeySource=\"" + loginSshProxyKeySource + "\" is not implemented yet. Cannot continue, will do test WITHOUT proxy.");
            }

            if (sshKeyInfo == null) {
                test.warn("Configuration error: Test option loginSshProxyKeySource=\"" + loginSshProxyKeySource + "\" is not unknown. Cannot continue, will do test WITHOUT proxy.");
            } else {
                JFedConnection.SshProxyInfo loginSshProxyInfo = new JFedConnection.SshProxyInfo(
                        loginSshProxyHostName,
                        loginSshProxyHostPort,
                        username,
                        sshKeyInfo,
                        loginSshProxyHostKey,
                        limitName);
                test.note("Using SSH proxy for SSH login test: " + loginSshProxyInfo);

                nodeLoginTester.setSshProxy(loginSshProxyInfo);
            }
        }

        if (Objects.equals(mode, "user")) {
            GeniUser user = test.getUser();
            Server userAuth = user.getUserAuthorityServer();

            if (userAuth.getTestbed().getProxies().isEmpty()) {
                test.warn("This logged in user's authority does not have a proxy that jFed can use.");
                return;
            }

            Proxy authProxyInfo = userAuth.getTestbed().getProxies().get(0);

            VlanRange portRange = new VlanRange(authProxyInfo.getPortRange());
            Integer port = portRange.getFirst();

//            while (port != null && unreachableProxies.contains(new ProxyInfo(authProxyInfo.getHostname(), port))) {
//                port = portRange.getNext();
//            }
//            if (port == null) {
//                LOG.debug("Proxy at " + authProxyInfo.getHostname() + " has no reachable ports, skipping.");
//                continue;
//            }
//            LOG.info("SSH Proxy is activated, and there is a reachable SSH Proxy for user authority " + userAuth.getName() + ": " + authProxyInfo.getHostname() + ":" + port);

            SshKeyInfo userSshKeyInfo = SshKeyInfoFactory.createGeniUserSshKeyInfo(test.getUser());

            JFedConnection.SshProxyInfo proxyInfo = new JFedConnection.SshProxyInfo(
                    authProxyInfo.getHostname(),
                    port,
                    user.getUserUrn().getEncodedResourceName(),
                    userSshKeyInfo,
                    authProxyInfo.getHostKey(),
                    true);

            nodeLoginTester.setSshProxy(proxyInfo);
        }
    }

    public void selectRegularTestCredentialType(ApiTest test) {
        testCredentialType = TestCredentialType.REGULAR;
        speaksForConfig = null;
        secondUserConfig = null;

        delegatedUserCredential = null;
        speaksForUserCredential = null;
        user2Credentials = null;
        for (SliceInfo sliceInfo : allSliceInfo)
            sliceInfo.delegatedCredential = null;

        userAndSliceApiWrapperUser2 = null;
    }

//    /**
//     * user 1 is speaking for user 2
//     */
//    public void selectSpeaksForTestCredentialType(ApiTest test, GeniUser secondUser) {
//        testCredentialType = TestCredentialType.SPEAKSFOR;
//        this.secondUser = secondUser;
//        delegatedUserCredential = null;
//        speaksForUserCredential = null;
//        user2Credentials = null;
//        for (SliceInfo sliceInfo : allSliceInfo)
//            sliceInfo.delegatedCredential = null;
//
//        userAndSliceApiWrapperUser2 =
//                test.getAutomaticUserAndSliceApiWrapperFactory().create(secondUser);
//
//        test.note("User 1=" + test.getUser().getUserUrnString() + " speaks for user 2=" + secondUser.getUserUrnString() + "");
//    }

//    /**
//     * user 1 uses delegated credential from user 2 (so user 2's rights)
//     */
//    public void selectDelegationTestCredentialType(ApiTest test, GeniUser secondUser) {
//        testCredentialType = TestCredentialType.DELEGATION;
//        this.secondUser = secondUser;
//        delegatedUserCredential = null;
//        speaksForUserCredential = null;
//        user2Credentials = null;
//        for (SliceInfo sliceInfo : allSliceInfo)
//            sliceInfo.delegatedCredential = null;
//
//        userAndSliceApiWrapperUser2 =
//                test.getAutomaticUserAndSliceApiWrapperFactory().create(secondUser);
//
//        test.note("User 2=" + secondUser.getUserUrnString() + " delegates rights to user 1=" + test.getUser().getUserUrnString() + "");
//    }

    public SfaCredential getDelegatedUserCredential() {
        if (delegatedUserCredential != null) return delegatedUserCredential;
//        if (userCredential == null) getUserCredential();
//        if (userCredential == null) throw new RuntimeException("User credential is null");
        if (user2Credentials == null || user2Credentials.isEmpty()) getUser2Credentials();
        if (user2Credentials == null || user2Credentials.isEmpty())
            throw new RuntimeException("User credential is null");

        GeniUser user1 = test.getUser();
        GeniUser user2 = secondUserConfig.getUser();

        SfaCredential sfaUser2Credential = null;
        for (AnyCredential user2Credential : user2Credentials)
            if (user2Credential instanceof SfaCredential)
                sfaUser2Credential = (SfaCredential) user2Credential;
        if (sfaUser2Credential == null) {
            throw new RuntimeException("No SFA user 2 credential available: " + user2Credentials);
        }
        try {
            delegatedUserCredential = sfaUser2Credential.delegate(
                    user1.getUserUrnString()/*newOwnerUrn*/,
                    user1.getClientCertificateChain().get(0)/*newOwnerCert*/,
                    user2.getPrivateKey()/*originalOwnerPrivateKey*/,
                    new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)/*expireDate*/,
                    "*"/*delegatedRights*/,
                    false/*canDelegate*/);
        } catch (CredentialException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
        assert delegatedUserCredential != null;
        return delegatedUserCredential;
    }

//    public AnyCredential getSpeaksForUserCredential() {
//        if (speaksForUserCredential != null) return speaksForUserCredential;
////        if (user2Credential == null) getUser2Credential();
////        if (user2Credential == null) throw new RuntimeException("User credential is null");
//
//        GeniUser user1 = test.getUser();
//        GeniUser user2 = secondUserConfig.getUser();
//
//        try {
//            speaksForUserCredential = SfaCredential.createSpeaksFor(
//                    user2.getUserUrnString()/*spokenForUrn*/,
//                    user1.getUserUrnString()/*speakerUrn*/,
//                    user2.getClientCertificateChain().get(0)/*spokenForCert*/,
//                    user1.getClientCertificateChain().get(0)/*speakerCert*/,
//                    user2.getPrivateKey()/*spokenForPrivateKey*/,
//                    new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)/*expireDate*/,
//                    "*"/*privilegeName*/,
//                    false/*canDelegate*/);
//        } catch (CredentialException e) {
//            throw new RuntimeException(e.getMessage(), e);
//        }
//        assert speaksForUserCredential != null;
//        return speaksForUserCredential;
//    }

    public SliceInfo createSlice(String prefix, String project, boolean preferUsingProject,
                                 boolean supportsFixedRspecOption) throws JFedException, IOException {
        return createSlice(prefix, project, preferUsingProject, supportsFixedRspecOption, 60);
    }

    /**
     * @param prefix             a prefix for the generated unique slice name
     * @param project            the project/subauthroity, may be null
     * @param preferUsingProject even if not specified in "project" and not mandatory, try to use a project. But don't use one if none can be found.
     * @return
     * @throws JFedException
     */
    public SliceInfo createSlice(String prefix, String project, boolean preferUsingProject,
                                 boolean supportsFixedRspecOption, int sliceExpireTimeInMinutes) throws JFedException, IOException {
        return createSlice(prefix, true/*createName*/, project, preferUsingProject, supportsFixedRspecOption, sliceExpireTimeInMinutes);
    }

    public static void addOptionsForFixedRspec(List<String> res) {
        res.add("fixed_rspec"); //the rspec to use
        res.add("fixed_rspec_file"); //file containing the rspec to use
        res.add("fixed_rspec_url"); //url containing the rspec to use
        res.add("fixed_rspec_bind_unbound_to"); //what to bind unbound nodes to. Use "auto" to bind them to the target of the test.
        res.add("fixed_rspec_set_component_urn"); //find the first node in the RSpec, and set the coponent_urn
    }

    public String bindUnboundTo(String rspec, String target) {
        if (target == null || target.trim().isEmpty() || rspec == null) return rspec;

        final RequestRspecSource requestRspecSource = new RequestRspecSource(rspec, ModelRspecType.BASIC);
        BasicModelRspec model = requestRspecSource.getModelRspec(ModelRspecType.BASIC);
        if (model == null) {
            test.warn("Could not parse provided RSpec. Will not try to bind unbound nodes.");
            return rspec;
        }
        if (requestRspecSource.isLosingData(ModelRspecType.BASIC)) {
            test.warn("Something is wrong in RSpec. Because of this, the unbound nodes cannot be bound without losing data. Will not bind nodes.");
            return rspec;
        }

        String targetComponentManagerId = target;
        if (target.trim().equalsIgnoreCase("auto") || target.trim().equalsIgnoreCase("target")) {
            targetComponentManagerId = test.getTestedAuthority().getRspecUrn().toString();
            test.note("Will bind unbound nodes to target of this test: \"" + targetComponentManagerId + "\"");
        } else {
            if (GeniUrn.parse(targetComponentManagerId) == null) {
                test.warn("The configured target for unbound nodes (\"fixed_rspec_bind_unbound_to\" option) is not a valid URN (and not \"auto\"): \"" + targetComponentManagerId + "\". Will not bind nodes.");
                return rspec;
            }
            test.note("Will bind unbound nodes to: \"" + targetComponentManagerId + "\"");
        }
        GeniUrn targetComponentManagerIdUrn = GeniUrn.parse(targetComponentManagerId);
        assert targetComponentManagerIdUrn != null;

        //TODO: check if there is a generic method to bind that can be used instead.
        for (BasicRspecNode node : model.getNodes()) {
            if (node.getComponentManagerId() == null) {
                node.setComponentManagerId(targetComponentManagerId);

                node.getLinks().stream()
                        .filter(link -> !link.getComponentManagerUrns().contains(targetComponentManagerIdUrn))
                        .forEach(link -> link.getComponentManagerUrns().add(targetComponentManagerIdUrn));
            }
        }

        return model.toGeni3Rspec();
    }

    public String forceComponentUrn(String rspec, String componentUrn) {
        if (componentUrn == null || componentUrn.trim().isEmpty() || rspec == null) return rspec;

        final RequestRspecSource requestRspecSource = new RequestRspecSource(rspec, ModelRspecType.BASIC);
        BasicModelRspec model = requestRspecSource.getModelRspec(ModelRspecType.BASIC);
        if (model == null) {
            test.warn("Could not parse provided RSpec. Will not try to force component_urn.");
            return rspec;
        }
        if (requestRspecSource.isLosingData(ModelRspecType.BASIC)) {
            test.warn("Something is wrong in RSpec. Because of this, the component_urn cannot be set without losing data. Will not force component_urn.");
            return rspec;
        }

        if (GeniUrn.parse(componentUrn) == null) {
            test.warn("The configured target to force component_urn (\"force fixed_rspec_set_component_urn\" option) is not a valid URN: \"" + componentUrn + "\". Will not force component_urn.");
            return rspec;
        }
        test.note("Will force component_urn of first node to: \"" + componentUrn + "\"");

        GeniUrn targetComponentIdUrn = GeniUrn.parse(componentUrn);
        assert targetComponentIdUrn != null;

        BasicRspecNode node0 = model.getNodes().get(0);
        node0.setComponentId(targetComponentIdUrn);

        return model.toGeni3Rspec();
    }

    private boolean hasCachedFixedRspec = false;
    private String cachedFixedRspec = null;
    private boolean cachedIsGeniFixedRspec = true;

    public String getFixedRspecOption() throws IOException {
        if (!hasCachedFixedRspec) {
            cachedFixedRspec = getFixedRspecOptionInternal();
            hasCachedFixedRspec = true;
        }
        return cachedFixedRspec;
    }

    public boolean isFixedRspec() throws IOException {
        if (!hasCachedFixedRspec) {
            cachedFixedRspec = getFixedRspecOptionInternal();
            hasCachedFixedRspec = true;
        }
        return cachedFixedRspec != null;
    }

    /**
     * If there is a fixed RSpec, is it a valid geni v3 RSpec, or a non geni RSpec?
     */
    public boolean isGeniFixedRspec() throws IOException {
        if (!hasCachedFixedRspec) {
            cachedFixedRspec = getFixedRspecOptionInternal();
            hasCachedFixedRspec = true;
        }
        return cachedIsGeniFixedRspec;
    }

    public String getFixedRspecOptionInternal() throws IOException {
        String rspecArg = test.getTestConfig().getProperty("fixed_rspec");
        final String rspecFileArg = test.getTestConfig().getProperty("fixed_rspec_file");
        final String rspecUrlArg = test.getTestConfig().getProperty("fixed_rspec_url");
        final String rspecBindUnboundArg = test.getTestConfig().getProperty("fixed_rspec_bind_unbound_to");
        final String forceComponentUrn = test.getTestConfig().getProperty("fixed_rspec_set_component_urn");

        int countRspecArgs = 0;
        if (rspecArg != null) countRspecArgs++;
        if (rspecFileArg != null) countRspecArgs++;
        if (rspecUrlArg != null) countRspecArgs++;

        if (countRspecArgs > 1)
            throw new RuntimeException("There are three options that control the use of a fixed_rspec: fixed_rspec, fixed_rspec_file and fixed_rspec_url. You provided more than 1, which is not allowed.");

        String res = null;

        if (rspecArg != null) {
            if (rspecArg.trim().isEmpty()) {
                test.warn("Empty Rspec specified in config");
                return null;
            }
            res = rspecArg;
        }

        if (rspecFileArg != null) {
            String rspec = IOUtils.fileToString(rspecFileArg);
            if (rspec == null || rspec.trim().isEmpty()) {
                test.warn("Empty Rspec read from file \"" + rspecFileArg + "\"");
                return null;
            }
            res = rspec;
        }

        if (rspecUrlArg != null) {
            URL url = new URL(rspecUrlArg);
            try (InputStream is = url.openStream()) {
                String rspec = IOUtils.streamToString(is, "UTF-8");
                if (rspec == null || rspec.trim().isEmpty()) {
                    test.warn("Empty Rspec read from URL \"" + rspecUrlArg + "\"");
                    return null;
                }

                String head = TextUtil.abbreviate(rspec, 200);
                test.note("Downloaded Rspec from \"" + rspecUrlArg + "\". Rspec size: " + rspec.length() + " characters.\nHead: " + head, true/*formatAsCode*/);
                res = rspec;
            }
        }

        if (res != null)
            try {
                BasicStringRspec basicStringRspec = new BasicStringRspec(res);
                cachedIsGeniFixedRspec = basicStringRspec.isValidRspec();
            } catch (Exception e) {
                cachedIsGeniFixedRspec = false;
            }

        if (res != null && rspecBindUnboundArg != null && !rspecBindUnboundArg.trim().isEmpty()) {
            if (!cachedIsGeniFixedRspec)
                test.warn("Test options specify an authority to bind unbound nodes to (" + rspecBindUnboundArg + "), but the provided fixed RSpec is NOT a geni 3 RSpec: cannot bind!");
            else
                res = bindUnboundTo(res, rspecBindUnboundArg);
        }

        if (res != null && forceComponentUrn != null && !forceComponentUrn.trim().isEmpty()) {
            if (!cachedIsGeniFixedRspec)
                test.warn("Test options specify an component_urn to set the first node to (" + forceComponentUrn + "), but the provided fixed RSpec is NOT a geni 3 RSpec: cannot set!");
            else
                res = forceComponentUrn(res, forceComponentUrn);
        }

        return res;
    }


    public void registerSlivers(SliceInfo slice, Server authority, Collection<GeniUrn> sliverUrns, Date expirationDate) throws JFedException {
        if (testCredentialType == TestCredentialType.REGULAR ||
                (testCredentialType == TestCredentialType.SPEAKSFOR && speaksForConfig.isSARequestMethodSpeaksFor())
                ) {
            userAndSliceApiWrapperUser1.registerAggregatesForSlice(test.getLogger(), slice.credentials, slice.urn, authority.getDefaultComponentManagerAsGeniUrn(), sliverUrns, expirationDate);
        } else {
            userAndSliceApiWrapperUser2.registerAggregatesForSlice(test.getLogger(), slice.credentials, slice.urn, authority.getDefaultComponentManagerAsGeniUrn(), sliverUrns, expirationDate);
        }
    }

    public void unregisterSlivers(SliceInfo slice, Server authority, Collection<GeniUrn> sliverUrns) throws JFedException {
        if (testCredentialType == TestCredentialType.REGULAR ||
                (testCredentialType == TestCredentialType.SPEAKSFOR && speaksForConfig.isSARequestMethodSpeaksFor())
                ) {
            userAndSliceApiWrapperUser1.unregisterAggregatesForSlice(test.getLogger(), slice.credentials, slice.urn, authority.getDefaultComponentManagerAsGeniUrn(), sliverUrns);
        } else {
            userAndSliceApiWrapperUser2.unregisterAggregatesForSlice(test.getLogger(), slice.credentials, slice.urn, authority.getDefaultComponentManagerAsGeniUrn(), sliverUrns);
        }
    }


    public SliceInfo createSlice(String prefixOrName, boolean createName, String project, boolean preferUsingProject,
                                 boolean supportsFixedRspecOption) throws JFedException, IOException {
        return createSlice(prefixOrName, createName, project, preferUsingProject, supportsFixedRspecOption, 60);
    }

    /**
     * @param prefixOrName
     * @param createName
     * @param project                  the project/subauthroity, may be null
     * @param preferUsingProject       even if not specified in "project" and not mandatory, try to use a project. But don't use one if none can be found.
     * @param supportsFixedRspecOption if true, the fixed_rspec or fixed_rspec_file option is used. That means that nodes can have more than 3 characters. This will be taken into account.
     * @return
     * @throws JFedException
     */
    public SliceInfo createSlice(String prefixOrName, boolean createName, String project, boolean preferUsingProject,
                                 boolean supportsFixedRspecOption, int sliceExpireTimeInMinutes) throws JFedException, IOException {
//        boolean callAsUser1 = testCredentialType == TestCredentialType.REGULAR;
//        assert callAsUser1 != testCredentialType == TestCredentialType.SPEAKSFOR || testCredentialType == TestCredentialType.DELEGATION;

        test.note("testCredentialType = " + testCredentialType);
        if (testCredentialType == TestCredentialType.SPEAKSFOR && speaksForConfig.isSARequestMethodSpeaksFor()) {
            List<AnyCredential> speaksForCreds = speaksForConfig.getSpeaksForCredentials();
            assert speaksForCreds != null && !speaksForCreds.isEmpty();
            test.note("Activating speaksFor Credential");
            if (speaksForCreds == null || speaksForCreds.isEmpty())
                test.warn("Error activating speaksFor credential");
            else {
                userAndSliceApiWrapperUser1.setSpeaksFor(speaksForCreds, speaksForConfig.getUser2Urn());
                if (!speaksForConfig.isAutomaticSpeaksForOptionNames())
                    userAndSliceApiWrapperUser1.setExtraOptionsForCallsWithCredential(addCredentialExtraOptions(null));
            }
        }

        Date expirationDate = new Date(System.currentTimeMillis() + (sliceExpireTimeInMinutes * 60 * 1000L));

        int maxNodeNameLength = 3; //auto generated node names have a max length of 3 characters
        if (supportsFixedRspecOption) {
            String fixedRspec = getFixedRspecOption();
            if (fixedRspec != null && isGeniFixedRspec()) {
                RequestRspecSource source = new RequestRspecSource(fixedRspec, ModelRspecType.BASIC);
                test.assertNotNull(source.getBasicNodeInfo(), "Problem parsing RSpec. RSpec head=" + TextUtil.abbreviate(fixedRspec, 200));
                for (BasicStringRspec.BasicNodeInfo basicNodeInfo : source.getBasicNodeInfo()) {
                    String nodeName = basicNodeInfo.getClientId() != null ? basicNodeInfo.getClientId().trim() : null;
                    if (nodeName != null && nodeName.length() > maxNodeNameLength)
                        maxNodeNameLength = nodeName.length();
                }
            }
        }

        SliceInfo res = new SliceInfo();
        int tries = 0;
        boolean success = false;
        while (!success) {
            if (createName)
                res.name = CommonAMTest.createSliceName(test, userAuthority,
                        ApiInfo.getDefaultComponentManagerForEach(testedAuthoritiesForRspec),
                        prefixOrName, maxNodeNameLength, speaksForConfig);
            else
                res.name = prefixOrName;
            res.urn =
                    GeniUrn.createGeniUrnFromEncodedParts(userAuthority.getUrnTld(), "slice", res.name);  //not always correct, but overwritten below
            res.urnString = res.urn.toString();


            try {
                if (testCredentialType == TestCredentialType.REGULAR ||
                        (testCredentialType == TestCredentialType.SPEAKSFOR && speaksForConfig.isSARequestMethodSpeaksFor())
                        ) {
                    GeniUser user1 = test.getUser();

                    UserAndSliceApiWrapper.SubAuthoritySupport user1SubAuthoritySupport =
                            userAndSliceApiWrapperUser1.getSubAuthoritySupport(test.getLogger());

                    if (!userAndSliceApiWrapperUser1.hasUserCredentials()) {
                        GeniUrn userUrn;
                        if (testCredentialType == TestCredentialType.SPEAKSFOR) {
                            assert speaksForConfig.isSARequestMethodSpeaksFor();
                            userUrn = speaksForConfig.getUser2Urn();
                        } else
                            userUrn = test.getUser().getUserUrn();

                        userAndSliceApiWrapperUser1.getUserCredentials(test.getLogger(), userUrn);
                    }

                    UserAndSliceApiWrapper.SliceInfo sliceInfo;
                    if (user1SubAuthoritySupport == UserAndSliceApiWrapper.SubAuthoritySupport.SUB_AUTHORITY_MANDATORY || preferUsingProject) {
                        String subAuthorityName;
                        if (project == null) {
                            List<String> subAuthorityNames;
                            try {
                                GeniUrn userUrn;
                                if (testCredentialType == TestCredentialType.SPEAKSFOR) {
                                    assert speaksForConfig.isSARequestMethodSpeaksFor();
                                    userUrn = speaksForConfig.getUser2Urn();
                                } else
                                    userUrn = test.getUser().getUserUrn();
                                subAuthorityNames =
                                        userAndSliceApiWrapperUser1.getSubAuthorityNames(test.getLogger(), userUrn);
                            } catch (Exception t) {
                                LOG.warn("Exception while getting sub authority names. will try to ignore.", t);
                                test.note("Exception while getting sub authority names. will try to ignore. Exception: " + t.getMessage());
                                subAuthorityNames = new ArrayList<>();
                            }

                            if (subAuthorityNames.isEmpty()) {
                                if (Objects.equals(user1SubAuthoritySupport, UserAndSliceApiWrapper.SubAuthoritySupport.SUB_AUTHORITY_MANDATORY))
                                    throw new RuntimeException("There are no sub authorities for user " + user1.getUserUrn());
                                else
                                    subAuthorityName = project;
                            } else {
                                subAuthorityName = subAuthorityNames.get(0);
                            }
                        } else
                            subAuthorityName = project;

                        sliceInfo =
                                userAndSliceApiWrapperUser1.createSlice(test.getLogger(), res.name, expirationDate, subAuthorityName);
                    } else {
                        String subAuthorityName = project; //may be null
                        sliceInfo =
                                userAndSliceApiWrapperUser1.createSlice(test.getLogger(), res.name, expirationDate, subAuthorityName);
                    }
                    res.credentials = sliceInfo.getCredentials();
                    res.urn = sliceInfo.getUrn();
                    res.urnString = res.urn.toString();
                } else {
                    UserAndSliceApiWrapper.SubAuthoritySupport user2SubAuthoritySupport =
                            userAndSliceApiWrapperUser2.getSubAuthoritySupport(test.getLogger());

                    GeniUser user2 = secondUserConfig.getUser();

                    if (!userAndSliceApiWrapperUser2.hasUserCredentials())
                        userAndSliceApiWrapperUser2.getUserCredentials(test.getLogger(), user2.getUserUrn());

                    UserAndSliceApiWrapper.SliceInfo sliceInfo;
                    if (user2SubAuthoritySupport.equals(UserAndSliceApiWrapper.SubAuthoritySupport.SUB_AUTHORITY_MANDATORY) || preferUsingProject) {
                        String subAuthorityName;
                        if (project == null) {
                            List<String> subAuthorityNames;
                            try {
                                subAuthorityNames =
                                        userAndSliceApiWrapperUser2.getSubAuthorityNames(test.getLogger(), user2.getUserUrn());
                            } catch (Exception t) {
                                LOG.warn("Exception while getting sub authority names. will try to ignore.", t);
                                test.note("Exception while getting sub authority names. will try to ignore. Exception: " + t.getMessage());
                                subAuthorityNames = new ArrayList<>();
                            }
                            if (subAuthorityNames.isEmpty()) {
                                if (Objects.equals(user2SubAuthoritySupport, UserAndSliceApiWrapper.SubAuthoritySupport.SUB_AUTHORITY_MANDATORY))
                                    throw new RuntimeException("There are no sub authorities for user " + user2.getUserUrn());
                                else
                                    subAuthorityName = project;
                            } else {
                                subAuthorityName = subAuthorityNames.get(0);
                            }
                        } else
                            subAuthorityName = project;
                        sliceInfo =
                                userAndSliceApiWrapperUser2.createSlice(test.getLogger(), res.name, expirationDate, subAuthorityName);
                    } else {
                        String subAuthorityName = project; //may be null
                        sliceInfo =
                                userAndSliceApiWrapperUser2.createSlice(test.getLogger(), res.name, expirationDate, subAuthorityName);
                    }

                    res.credentials = sliceInfo.getCredentials();
                    res.urn = sliceInfo.getUrn();
                    res.urnString = res.urn.toString();
                }
                success = true;
            } catch (Exception e) {
                //ignore
                LOG.warn("Exception creating slice, will ignore for now", e);
                success = false;
            }

            if (!success) {
                //maybe name is taken

                tries++;
                test.assertTrue(tries <= MAX_CREATE_SLICE_TRIES, "tried " + tries + " times. Maximum tries is " + MAX_CREATE_SLICE_TRIES + ". -> Giving up!");
                if (tries > MAX_CREATE_SLICE_TRIES) {
                    res.credentials = null;
                    break;
                }

                //wait before trying again
                try {
                    Thread.sleep(1200 /*ms*/);
                } catch (InterruptedException ignored) {
                }
            }
        }
        test.assertNotNull(res.credentials);
        test.assertNotEmpty(res.credentials);

        //old version without UserAndSliceApiWrapper
//        ProtogeniSliceAuthority.SliceAuthorityReply<AnyCredential> registerRes = null;
//
//        int tries = 0;
//        boolean success = false;
//        while (!success) {
//            res.name = CommonAMTest.createSliceName(test, userAuthority, prefix);
//            res.urn = new GeniUrn(userAuthority.getUrnTld(), "slice", res.name);
//            res.urnString = res.urn.toString();
//
//            if (callAsUser1)
//                registerRes = sa.register(getSAConnection(), getUserCredential(), res.urn);
//            else
//                registerRes = sa.register(getSAUser2Connection(), getUser2Credential(), res.urn);
//            TestSliceAuthority.testSACorrectnessXmlRpcResult(registerRes.getRawResult());
//            success = registerRes.getGeniResponseCode().isSuccess();
//
//            if (!success) {
//                //maybe name is taken
//                test.note("Could not create slice \"" + res.urnString + "\": " + registerRes.getGeniResponseCode() + " output: " + registerRes.getOutput());
//
//                tries++;
//                test.assertTrue(tries <= MAX_CREATE_SLICE_TRIES, "tried "+tries+" times. Maximum tries is "+MAX_CREATE_SLICE_TRIES+". -> Giving up!");
//                if (tries > MAX_CREATE_SLICE_TRIES)
//                    break;
//
//                //wait before trying again
//                try { Thread.sleep(1200 /*ms*/); } catch (InterruptedException e) { }
//            }
//        }
//
//        test.assertNotNull(registerRes);
//        test.assertTrue(registerRes.getGeniResponseCode().isSuccess(),"Register call was not successful in creating \""+res.urn.getValue()+"\": "+registerRes.getGeniResponseCode()+" output="+registerRes.getOutput());
//        res.credential = registerRes.getValue();
//        test.assertNotNull(res.credential);
//        test.assertNotNull(res.credential.getCredentialXml());
//        test.assertTrue(res.credential.getCredentialXml().length() > 10, "credential too short "+ res.credential.getCredentialXml());

        test.assertNotNull(res.credentials);
        test.assertNotEmpty(res.credentials);

        allSliceInfo.add(res);

        logParsableInfo("SLICE_NAME", res.name);
        logParsableInfo("SLICE_URN", res.urnString);
        if (res.credentials != null && res.credentials.size() > 0) {
            try {
                for (AnyCredential c : res.credentials)
                    if (c instanceof SfaCredential) {
                        SfaCredential sc = (SfaCredential) c;
                        if (sc.getTargetGid() != null && !sc.getTargetGid().trim().isEmpty()) {
                            Gid gid = new Gid(sc.getTargetGid());
                            if (gid.getSubjectUuid() != null)
                                logParsableInfo("SLICE_UUID", gid.getSubjectUuid());
                        }

                        GeniUrn targetUrn = GeniUrn.parse(sc.getTargetUrn());
                        test.assertNotNull(targetUrn, "The slice credential does not have a valid target URN: " + sc.getTargetUrn());
                        if (targetUrn.getEncodedResourceType().equalsIgnoreCase("auth")
                                || targetUrn.getEncodedResourceType().equalsIgnoreCase("auth")
                                || targetUrn.getEncodedResourceType().equalsIgnoreCase("user")) {
                            test.fatalError("The slice credential is not a slice URN: " + sc.getTargetUrn());
                        }
                        if (!targetUrn.getEncodedResourceType().equalsIgnoreCase("slice"))
                            test.warn("The slice credential is probably not a slice URN: " + sc.getTargetUrn());
                    }
            } catch (Exception t) {
                //safe to ignore: We don't want this unimportant info retrieval to break any test
                LOG.error("Error retrieving slice UUID info", t);
                test.note("Debug info: Error retrieving slice UUID info: " + t.getMessage());
            }
        }

        return res;
    }

    /**
     * @return the SliceInfo, or null if the slice does not exist.
     */
    public SliceInfo reuseSlice(String sliceName) throws JFedException {
        boolean callAsUser1 = testCredentialType == TestCredentialType.REGULAR;
        assert callAsUser1 != (testCredentialType == TestCredentialType.SPEAKSFOR || testCredentialType == TestCredentialType.DELEGATION);

        if (testCredentialType == TestCredentialType.SPEAKSFOR && speaksForConfig.isSARequestMethodSpeaksFor())
            callAsUser1 = true;

        SliceInfo res = new SliceInfo();
        int tries = 0;
        res.name = sliceName;
        res.urn = null; //could be sub authority slice, so we don't know urn!
        res.urnString = null;

        try {
            if (callAsUser1) {
                GeniUrn userUrn;
                if (testCredentialType == TestCredentialType.SPEAKSFOR) {
                    assert speaksForConfig.isSARequestMethodSpeaksFor();
                    userUrn = speaksForConfig.getUser2Urn();
                } else
                    userUrn = test.getUser().getUserUrn();
                if (!userAndSliceApiWrapperUser1.hasUserCredentials())
                    userAndSliceApiWrapperUser1.getUserCredentials(test.getLogger(), userUrn);
                List<GeniUrn> sliceUrns =
                        userAndSliceApiWrapperUser1.getSlicesForUser(test.getLogger(), userUrn);

                //could be sub authority slice
                for (GeniUrn sliceUrn : sliceUrns) {
                    if (Objects.equals(sliceUrn.getEncodedResourceName(), sliceName)) {
                        res.urn = sliceUrn;
                        break;
                    }
                }

                //no slice by that name
                if (res.urn == null)
                    return null;

                res.credentials = userAndSliceApiWrapperUser1.getSliceCredentials(test.getLogger(), res.urn);
            } else {
                List<GeniUrn> sliceUrns =
                        userAndSliceApiWrapperUser2.getSlicesForUser(test.getLogger(), secondUserConfig.getUser().getUserUrn());

                //could be sub authority slice
                for (GeniUrn sliceUrn : sliceUrns) {
                    if (Objects.equals(sliceUrn.getEncodedResourceName(), sliceName)) {
                        res.urn = sliceUrn;
                        break;
                    }
                }

                //no slice by that name
                if (res.urn == null)
                    return null;

                res.credentials = userAndSliceApiWrapperUser2.getSliceCredentials(test.getLogger(), res.urn);
            }
        } catch (Exception e) {
            //ignore
            LOG.info("Exception trying to reuse slice, will ignore", e);
            test.note("Exception trying to reuse slice, will ignore", e);
            return null;
        }

        test.assertNotNull(res.urn);
        res.urnString = res.urn.toString();

        test.assertNotNull(res.credentials);
        test.assertNotEmpty(res.credentials);

        allSliceInfo.add(res);

        return res;
    }

    public SliceInfo renewSlice(SliceInfo sliceInfo, Date newExpirationtime) throws JFedException {
        //this just changes the actual sliceInfo in place

        boolean callAsUser1 = testCredentialType == TestCredentialType.REGULAR;
        assert callAsUser1 != (testCredentialType == TestCredentialType.SPEAKSFOR || testCredentialType == TestCredentialType.DELEGATION);

        if (callAsUser1) {
            sliceInfo.credentials =
                    userAndSliceApiWrapperUser1.renewSlice(test.getLogger(), sliceInfo.credentials, newExpirationtime);
        } else {
            sliceInfo.credentials =
                    userAndSliceApiWrapperUser2.renewSlice(test.getLogger(), sliceInfo.credentials, newExpirationtime);
        }

        test.assertNotNull(sliceInfo.credentials);
        test.assertNotEmpty(sliceInfo.credentials);

        return sliceInfo;
    }

    public static boolean sameNodesInRequestAndManifest(ApiTest test, String requestRspec, String manifestRspec) {
        List<BasicStringRspec.BasicNodeInfo> basicNodeInfoRequest =
                new BasicStringRspec(requestRspec).getBasicNodeInfo();
        List<BasicStringRspec.BasicNodeInfo> basicNodeInfoManifest =
                new BasicStringRspec(manifestRspec).getBasicNodeInfo();
        if (basicNodeInfoManifest.size() < basicNodeInfoRequest.size())
            return false;
        if (basicNodeInfoManifest.size() > basicNodeInfoRequest.size())
            test.warn("There are more nodes in manifest(" + basicNodeInfoManifest.size() + ") " +
                    "than in request(" + basicNodeInfoRequest.size() + ")!");
        boolean res = true;
        String clientIds = "";
        for (BasicStringRspec.BasicNodeInfo requestNode : basicNodeInfoRequest) {
            String clientId = requestNode.getClientId();
            clientIds += " \"" + clientId + "\"";
            if (clientId != null) {
                boolean found = false;
                for (BasicStringRspec.BasicNodeInfo manifestNode : basicNodeInfoManifest)
                    if (manifestNode.getClientId() != null && Objects.equals(manifestNode.getClientId(), clientId)) {
                        found = true;
                        break;
                    }
                if (!found) {
                    test.errorNonFatal("Request RSpec had node with client_id \"" + clientId + "\" but the received manifest does not have this node.");
                    res = false;
                }
            }
        }
        if (res)
            test.note("Successfully found the following client_id's in both request and manifest:" + clientIds);
        return res;
    }

    public static void addCredentialOptions(List<String> res) {
        res.add("credentialType"); //regular, speaksFor or delegation

        res.addAll(SpeaksForConfig.getFixedOptionNames());
    }

    public void processCredentialOptions(TestbedInfoSource testbedInfoSource, AuthorityFinder authorityFinder) {
        if (test.getTestConfig().get("credentialType") != null) {
            String credentialTypeParameter = (String) test.getTestConfig().get("credentialType");

            boolean known = false;
            boolean normal =
                    credentialTypeParameter.trim().equalsIgnoreCase("normal") || credentialTypeParameter.trim().equalsIgnoreCase("regular");
            boolean sf = credentialTypeParameter.trim().equalsIgnoreCase("speaksFor");
            boolean dele =
                    credentialTypeParameter.trim().equalsIgnoreCase("delegation") || credentialTypeParameter.trim().equalsIgnoreCase("delegated") || credentialTypeParameter.trim().equalsIgnoreCase("delegate");

            if (normal) {
                test.note("All tests will test regular credentials");
                selectRegularTestCredentialType(test);
            }
            if (dele) {
                testCredentialType = TestCredentialType.DELEGATION;
                test.note("All tests will test delegated credentials");

                throw new RuntimeException("Delegated credentials are no longer supported. Support needs to be reimplemented.");
            }
            if (sf) {
                test.note("All tests will test speaks-for credentials");
                testCredentialType = TestCredentialType.SPEAKSFOR;

                speaksForConfig = new SpeaksForConfig();
                speaksForConfig.process(test, test.getTestConfig(), testbedInfoSource, authorityFinder);

                if (speaksForConfig.isSARequestMethodLogin()) {
                    secondUserConfig = speaksForConfig.getUser2Config();
                    assert secondUserConfig != null;
                    assert secondUserConfig.getUser() != null;
                    userAndSliceApiWrapperUser2 =
                            test.getAutomaticUserAndSliceApiWrapperFactory().create(secondUserConfig.getUser());
                } else {
                    userAndSliceApiWrapperUser2 = null;
                    secondUserConfig = null;
                }
            }
            if (!sf && !dele && !normal)
                throw new RuntimeException("credentialType parameter has unrecognized value \"" + credentialTypeParameter + "\". Possible values: regular speaksFor delegation");
        } else {
            test.note("All tests will test regular credentials");
            selectRegularTestCredentialType(test);
        }
    }

    private enum TestCredentialType {REGULAR, SPEAKSFOR, DELEGATION}

    public static class SliceInfo {
        public String name;
        public GeniUrn urn;
        public List<AnyCredential> credentials;
        public SfaCredential delegatedCredential;
        public String urnString;

        public SfaCredential getDelegatedCredential(GeniUser receiverUser, GeniUser granterUser) {
            if (delegatedCredential != null) return delegatedCredential;
            if (credentials == null || credentials.isEmpty()) return null;

            SfaCredential sfaCredential = null;
            for (AnyCredential credential : credentials)
                if (credential instanceof SfaCredential)
                    sfaCredential = (SfaCredential) credential;
            if (sfaCredential == null)
                return null;

            try {
                delegatedCredential = sfaCredential.delegate(
                        receiverUser.getUserUrnString()/*newOwnerUrn*/,
                        receiverUser.getClientCertificateChain().get(0)/*newOwnerCert*/,
                        granterUser.getPrivateKey()/*originalOwnerPrivateKey*/,
                        new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)/*expireDate*/,
                        "*"/*delegatedRights*/,
                        false/*canDelegate*/);
            } catch (CredentialException e) {
                throw new RuntimeException(e.getMessage(), e);
            }
            return delegatedCredential;
        }
    }


    /**
     * Checks if the ping succeeded without packet loss, and outputs ping timing info.
     *
     * @param conn   the SSH connection. This is only used to request additional info when the ping fails.
     *               May be null (in which case no additional info will be requested)
     * @param result the received output of the ping command
     */
    public void checkPingResult(Session conn, String result) {
        if (result != null && result.length() > 5) {
            //example reply:
            //   5 packets transmitted, 3 received
            Pattern patternTrans = Pattern.compile("([0-9]*) packets transmitted");
            Matcher matcherTrans = patternTrans.matcher(result);
            test.assertTrue(matcherTrans.find(), "Did not find \"[0-9]* packets transmitted\" in result");
            String sent = matcherTrans.group(1);

            Pattern patternRecv = Pattern.compile("([0-9]*) received");
            Matcher matcherRecv = patternRecv.matcher(result);
            test.assertTrue(matcherRecv.find(), "Did not find \"[0-9]* received\" in result");
            String recv = matcherRecv.group(1);

            if (conn != null && (!Objects.equals(sent, "5") || !Objects.equals(recv, "5"))) {
                try {
                    String command = "echo; route - n; echo; echo; ifconfig -a";
                    Session.Command comm = conn.exec(command);
                    comm.join();
                    result = "";
                    String err = "";
                    try {
                        BufferedReader sout = new BufferedReader(new InputStreamReader(comm.getInputStream()));
                        BufferedReader serr = new BufferedReader(new InputStreamReader(comm.getErrorStream()));
                        String line = sout.readLine();
                        while (line != null) {
                            result += line + "\n";
                            line = sout.readLine();
                        }
                        line = serr.readLine();
                        while (line != null) {
                            err += line + "\n";
                            line = serr.readLine();
                        }
                    } finally {
                        comm.close();
                    }
                    test.note("Ping failed, gathering informational with: \"" + command + "\" command. result: \"" + result.trim() + "\". (stderr is: \"" + err + "\")", true/*formatAsCode*/);
                } catch (Exception e) {
                    test.note("Ping failed, also failed to gathering additional info: " + e.getMessage());
                    LOG.error("Ping failed, also failed to gathering additional info", e);
                }
            }

            test.assertEquals("5", sent, "Packets sent count is not 5: sent=\"" + sent + "\"  (recv=\"" + recv + "\")");
            test.assertEquals("5", recv, "Packets received count is not 5: \"" + recv + "\"");

            //Example timing info:  rtt min/avg/max/mdev = 0.294/0.391/0.678/0.145 ms"
            Pattern patternTiming = Pattern.compile("min/avg/max/mdev = ([0-9.]*)/([0-9.]*)/([0-9.]*)/([0-9.]*) ms");
            Matcher matcherTiming = patternTiming.matcher(result);
            if (matcherTiming.find()) {
                try {
                    Double min = Double.parseDouble(matcherTiming.group(1));
                    Double avg = Double.parseDouble(matcherTiming.group(2));
                    Double max = Double.parseDouble(matcherTiming.group(3));
                    Double mdev = Double.parseDouble(matcherTiming.group(4));
                    String pingTiming = min + "," + avg + "," + max + "," + mdev;
                    logParsableInfo("PING_RTT_MIN", min + "");
                    logParsableInfo("PING_RTT_MAX", max + "");
                    logParsableInfo("PING_RTT_AVG", avg + "");
                    logParsableInfo("PING_RTT_MDEV", mdev + "");
                    test.note("Ping timing (min,avg,max,mdev in ms): " + pingTiming);
                } catch (NumberFormatException e) {
                    test.note("Failed to parse a ping timing info double. Probably non-compatible ping version: " + e.getMessage());
                    LOG.warn("Failed to parse a ping timing info double. Probably non-compatible ping version", e);
                }
            } else {
                test.note("Failed to parse ping timing info. Probably non-compatible ping version.");
            }
        } else {
            test.errorNonFatal("No usable ping output seen.");
        }
    }

    public void logParsableInfo(String infoName, String infoValue) {
        String usedInfoName = infoName.trim().toUpperCase().replaceAll(" ", "_");
        test.note(HtmlLogOutput.MACHINE_LOGLINE_PREFIX + " BEGIN_" + usedInfoName + " " + infoValue + " END_" + usedInfoName);
    }

    public SpeaksForConfig getSpeaksForConfig() {
        return speaksForConfig;
    }

    public User2LoginConfig getSecondUserConfig() {
        return secondUserConfig;
    }

    public static class User2LoginConfig extends TestConfig {

        private GeniUser user;

        public static List<String> getFixedOptionNames() {
            List<String> res = new ArrayList<>();
            res.add("passwordFilename2");
            res.add("userUrn2");
            res.add("pemKeyAndCertFilename2");
            res.add("userAuthorityUrn2");
            return res;
        }

        @Override
        public List<String> getOptionNames() {
            return User2LoginConfig.getFixedOptionNames();
        }

        @Override
        public void process(ApiTest test, Properties properties, TestbedInfoSource testbedInfoSource, AuthorityFinder authorityFinder) {
            test.note("Processing config options for 2nd user");
            try {
                user = new ContextFile(properties, testbedInfoSource, authorityFinder).getUser("2");
            } catch (ContextFile.ContextFileException e) {
                throw new RuntimeException("Exception extracting user 2 details from context file", e);
            }
            test.assertNotNull(user);
            test.assertNotNull(user.getUserAuthorityServer());
        }

        public GeniUser getUser() {
            return user;
        }
    }

    public static class SpeaksForConfig extends TestConfig {
        public enum CredentialSource {JFED_GENERATED, USER_PROVIDED}

        public enum SARequestMethod {LOGIN, SPEAKSFOR}

        private boolean automaticSpeaksForOptionNames = true;
        private final List<String> speaksForOptionNames = new ArrayList<>();
        private User2LoginConfig user2Config;

        private CredentialSource credentialSource;
        private SARequestMethod saRequestMethod;

        private AnyCredential userProvidedCredential;
        private AnyCredential generatedCredential;

        private GeniUrn user2Urn; //urn of user spoken for
        private GeniUser user2Login;


        public void setSpeaksForOptionNames(List<String> newSpeaksForOptionNames) {
            speaksForOptionNames.clear();
            speaksForOptionNames.addAll(newSpeaksForOptionNames);
            automaticSpeaksForOptionNames = false;
        }

        public boolean isAutomaticSpeaksForOptionNames() {
            return automaticSpeaksForOptionNames;
        }

        public List<String> getSpeaksForOptionNames() {
            return speaksForOptionNames;
        }

        public CredentialSource getCredentialSource() {
            return credentialSource;
        }

        public SARequestMethod getSaRequestMethod() {
            return saRequestMethod;
        }

        public boolean isSARequestMethodLogin() {
            return saRequestMethod == SARequestMethod.LOGIN;
        }

        public boolean isSARequestMethodSpeaksFor() {
            return saRequestMethod == SARequestMethod.SPEAKSFOR;
        }

        public AnyCredential getUserProvidedCredential() {
            return userProvidedCredential;
        }

        public List<AnyCredential> getSpeaksForCredentials() {
            if (credentialSource == CredentialSource.USER_PROVIDED)
                return userProvidedCredential == null ? null : userProvidedCredential.toCredentialList();
            else {
                assert credentialSource == CredentialSource.JFED_GENERATED;
                return generatedCredential == null ? null : generatedCredential.toCredentialList();
            }
        }


        public User2LoginConfig getUser2Config() {
            return user2Config;
        }

        public GeniUser getUser2Login() {
            return user2Login;
        }

        public GeniUrn getUser2Urn() {
            return user2Urn;
        }

        public static List<String> getFixedOptionNames() {
            List<String> res = new ArrayList<>();
            res.add("speaksForOptionNames"); //options: "geni_speaking_for", "speaking_for" or any other, or comma seperated list of options. Can also be "none" or "automatic". Default: "automatic"

            res.add("saRequestMethod"); //LOGIN or SPEAKSFOR
            res.add("credentialSource");//JFED_GENERATED or USER_PROVIDED

            res.add("providedSpeaksForCredential");

            //note, required even without user2: res.add("userUrn2");
            res.addAll(User2LoginConfig.getFixedOptionNames());

            return res;
        }

        public List<String> getOptionNames() {
            return getFixedOptionNames();
        }

        @Override
        public void process(ApiTest test, Properties properties, TestbedInfoSource testbedInfoSource, AuthorityFinder authorityFinder) {

            if (properties.containsKey("speaksForOptionNames")) {
                String speaksForOptionNamesString = (String) properties.get("speaksForOptionNames");

                if (speaksForOptionNamesString.trim().equalsIgnoreCase("automatic")) {
                    test.note("SpeaksFor option names will be chosen automatically based on the API used.");
                    automaticSpeaksForOptionNames = true;
                } else {
                    if (speaksForOptionNamesString.trim().equalsIgnoreCase("none")) {
                        test.note("No speaksFor option will be used.");
                        automaticSpeaksForOptionNames = false;
                        speaksForOptionNames.clear();
                    } else {
                        String[] speaksForOptionNamesArray = speaksForOptionNamesString.trim().split(",");
                        speaksForOptionNames.clear();
                        automaticSpeaksForOptionNames = false;
                        for (String speaksForOptionName : speaksForOptionNamesArray) {
                            speaksForOptionName = speaksForOptionName.trim();
                            speaksForOptionNames.add(speaksForOptionName);
                        }
                        test.note("Setting speaksFor option names to use: " + speaksForOptionNames);
                    }
                }
            } else {
                //set both known speaksFor option names
                automaticSpeaksForOptionNames = true;
                speaksForOptionNames.clear();
//                speaksForOptionNames.add("geni_experimenter_urn");
//                speaksForOptionNames.add("geni_speaking_for");
                test.note("speaksFor option names to use: defaulting to automatically choosing based on API.");
            }


            String saRequestMethodString = "login";
            if (properties.containsKey("saRequestMethod"))
                saRequestMethodString = ((String) properties.get("saRequestMethod")).trim();
            saRequestMethod = SARequestMethod.valueOf(saRequestMethodString.toUpperCase());

            if (saRequestMethod == null)
                throw new RuntimeException("Invalid \"saRequestMethod\" option: \"" + saRequestMethodString + "\" (valid options are: " + Arrays.toString(SARequestMethod.values()) + ")");

            String credentialSourceString = "login";
            if (properties.containsKey("credentialSource"))
                credentialSourceString = ((String) properties.get("credentialSource")).trim();
            credentialSource = CredentialSource.valueOf(credentialSourceString.toUpperCase());

            if (credentialSource == null)
                throw new RuntimeException("Invalid \"credentialSource\" option: \"" + credentialSourceString + "\" (valid options are: " + Arrays.toString(CredentialSource.values()) + ")");


            if (saRequestMethod == SARequestMethod.LOGIN || credentialSource == CredentialSource.JFED_GENERATED) {
                if (!properties.containsKey("pemKeyAndCertFilename2"))
                    throw new RuntimeException("Missing \"pemKeyAndCertFilename2\" option: this option is required when " +
                            "option \"saRequestMethod\"=\"LOGIN\" or when option \"credentialSource\"=\"JFED_GENERATED\"");

                user2Config = new User2LoginConfig();
                user2Config.process(test, properties, testbedInfoSource, authorityFinder);
                user2Login = user2Config.getUser();
                user2Urn = user2Login.getUserUrn();

                if (credentialSource == CredentialSource.JFED_GENERATED) {
                    GeniUser user1 = test.getUser();
                    GeniUser user2 = user2Login;

                    try {
                        generatedCredential = SfaCredential.createSpeaksFor(
                                user2.getUserUrnString()/*spokenForUrn*/,
                                user1.getUserUrnString()/*speakerUrn*/,
                                user2.getClientCertificateChain().get(0)/*spokenForCert*/,
                                user1.getClientCertificateChain().get(0)/*speakerCert*/,
                                user2.getPrivateKey()/*spokenForPrivateKey*/,
                                new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)/*expireDate*/,
                                "*"/*privilegeName*/,
                                false/*canDelegate*/);
                    } catch (CredentialException e) {
                        throw new RuntimeException(e.getMessage(), e);
                    }
                }
            } else {
                //user 2 URN is still needed
                if (properties.containsKey("userUrn2")) {
                    String user2UrnString = ((String) properties.get("userUrn2")).trim();
                    try {
                        user2Urn = new GeniUrn(user2UrnString);
                    } catch (GeniUrn.GeniUrnParseException e) {
                        throw new RuntimeException("Failed to parse \"userUrn2\" option into a geni URN: \"" + user2UrnString + "\"", e);
                    }
                } else {
                    assert saRequestMethod != SARequestMethod.LOGIN;
                    assert credentialSource != CredentialSource.JFED_GENERATED;
                    if (automaticSpeaksForOptionNames || !speaksForOptionNames.isEmpty())
                        throw new RuntimeException("Missing \"userUrn2\" option: this option is required unless saRequestMethod != LOGIN AND credentialSource != JFED_GENERATED AND speaksForOptionNames = none");
                }
            }

            if (credentialSource == CredentialSource.USER_PROVIDED) {
                if (!properties.containsKey("providedSpeaksForCredential"))
                    throw new RuntimeException("Missing \"providedSpeaksForCredential\" option: this option is required when option \"credentialSource\"=\"USER_PROVIDED\"");

                String credentialXml = null;

                String providedSpeaksForCredential = ((String) properties.get("providedSpeaksForCredential")).trim();
                if (providedSpeaksForCredential.startsWith("/") || providedSpeaksForCredential.startsWith("file:")) {
                    try {
                        credentialXml = IOUtils.fileToString(providedSpeaksForCredential);
                    } catch (IOException e) {
                        throw new RuntimeException("Failed to read speaksfor credential from file \"" + providedSpeaksForCredential + "\"", e);
                    }
                }
                if (providedSpeaksForCredential.startsWith("<")) {
                    credentialXml = providedSpeaksForCredential;
                }

                if (credentialXml == null)
                    throw new RuntimeException("Invalid \"providedSpeaksForCredential\" option: this option must be either a filename or and XML credential");

                try {
                    userProvidedCredential =
                            AnyCredential.createAutoDetect("SpeaksFor Credential from test config", credentialXml);
                } catch (CredentialException e) {
                    throw new RuntimeException(e);
                }

                if (userProvidedCredential == null)
                    throw new RuntimeException("Problem with \"providedSpeaksForCredential\" option: failed to process credential.");

                test.note("User 1=" + test.getUser().getUserUrnString() + " speaks for another user (speaksFor credential has been provided)");
            }

            if (saRequestMethod == SARequestMethod.SPEAKSFOR) {
                //nothing special to configure

                if (credentialSource != CredentialSource.USER_PROVIDED) {
                    throw new RuntimeException("Bad speaksfor config: saRequestMethod=" + saRequestMethod +
                            " can only be combined with credentialSource=" + CredentialSource.USER_PROVIDED + " (but " + credentialSource + " is configured)");
                }

                if (userProvidedCredential == null) {
                    throw new RuntimeException("Bad speaksfor config: saRequestMethod=" + saRequestMethod +
                            " combined with credentialSource=" + credentialSource + " requires a speaksfor credential to be found, but this went wrong.");
                }
            }
        }
    }
}
