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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.JFedException;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.api.AbstractGeniAggregateManager;
import be.iminds.ilabt.jfed.lowlevel.api.StitchingComputationService;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.AggregateManagerWrapper;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.StatusDetails;
import be.iminds.ilabt.jfed.lowlevel.connection.BasicConnectionBuilder;
import be.iminds.ilabt.jfed.lowlevel.connection.HandleUntrustedCallback;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnectionProvider;
import be.iminds.ilabt.jfed.lowlevel.connection.SfaConnection;
import be.iminds.ilabt.jfed.lowlevel.stitching.StitchingDirector;
import be.iminds.ilabt.jfed.lowlevel.stitching.info.Hop;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.TargetAuthority;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * StitchingPingTest:
 * - create a stitched RSpec, contact SCS and do all needed CreateSlivers.
 * - Then test login on node and connectivity using ping.
 * - finally, Delete everything
 */
public class StitchingPingTest extends AbstractStitchingPingTest {
    protected static final org.slf4j.Logger LOG = LoggerFactory.getLogger(StitchingPingTest.class);

    protected HandleUntrustedCallback handleUntrustedCallback = null;


    @Inject
    public StitchingPingTest(Logger logger, TargetAuthority testedAuthority, GeniUserProvider geniUserProvider,
                             JFedConnectionProvider connectionProvider, TestbedInfoSource testbedInfoSource, JFedPreferences jFedPreferences) {
        super(logger, testedAuthority, geniUserProvider, connectionProvider, testbedInfoSource, jFedPreferences);
    }

    @Override
    public List<String> getReqConfKeys() {
        List<String> res = new ArrayList<>();
        res.add("stitchedAuthorityUrns"); //space seperated list
        return res;
    }

    @Override
    public List<String> getOptConfKeys() {
        return super.getOptConfKeys();
    }

    @Override
    public String getTestDescription() {
        return "Test Stitching between multiple authorities, including node login and ping test between nodes. This version uses the AggregateManagerWrapper, and is thus not limited to AMv2. (and will actually prefer AMv3 if available.)";
    }


    @Override
    public void setUp() throws IOException, JFedException {
        super.setUp();
    }

    @Test(hardDepends = {"getUserCredentials"}, groups = {"createslice"})
    public void createSlice() throws JFedException, IOException {
        super.createSlice();
    }


    @Test(groups = {"init"})
    public void generateRspec() throws JFedException, IOException {
        super.generateRspec();
    }

    @Test(hardDepends = {"createSlice", "generateRspec"}, groups = {"init"})
    public void initStitching() throws JFedException, NoSuchAlgorithmException, MalformedURLException {
        director = new StitchingDirector(testbedInfoSource, authorityFinder, expirationDate);

        super.initStitching();
    }

    @Test(hardDepends = {"initStitching"}, groups = {"scs"})
    public void callSCS() throws JFedException {
        assertNotNull(sliceInfo);
        assert rspecXmlStr != null;

        //contact stitcher
        StitchingComputationService scs = new StitchingComputationService(logger, jFedPreferences);
        SfaConnection con = getSCSConnection(scsServer);

        StitchingComputationService.SCSReply<StitchingComputationService.ComputePathResult> scsReply =
                scs.computePath(con, sliceInfo.urnString, rspecXmlStr, null);

        assertNotNull(scsReply);
        assertEquals(scsReply.getCode(), 0, "SCS call Result code is not 0 but " + scsReply.getCode());
        assertNotNull(scsReply.getValue());

        StitchingComputationService.ComputePathResult computePathResult = scsReply.getValue();

        director.setComputePathResult(computePathResult);

        assertTrue(director.getAllAuthorities().size() > 0);
        assertTrue(director.getHopsLeft().size() > 0);
        assertTrue(director.areAnyActionsLeft());
        assertEquals(director.getHopsLeft().size(), director.getAllAuthorities().size(),
                "director getHopsLeft().size() != getAllAuthorities().size()  -> " +
                        director.getHopsLeft().size() + " != " + director.getAllAuthorities().size()
        );
    }

    public void setInsecure() {
        handleUntrustedCallback =
                new BasicConnectionBuilder.INSECURE_TRUSTALL_HandleUntrustedCallback();
    }

    final List<Server> toProvision = new ArrayList<>();
    final List<Server> toWait = new ArrayList<>();
    final List<Server> toDescribe = new ArrayList<>();

    @Test(hardDepends = {"callSCS", "createSlice"}, groups = {"createslivernowait"})
    public void doStitchActions() throws JFedException, URISyntaxException, NoSuchAlgorithmException {
        assertNotNull(sliceInfo.credentials);

        String fixedSshPublicKeyFile = getTestConfig().getProperty("fixed_ssh_public_key_file");
        String fixedSshPrivateKeyFile = getTestConfig().getProperty("fixed_ssh_private_key_file");
        String fixedSshPrivateKeyPassword = getTestConfig().getProperty("fixed_ssh_private_key_password");

        setInsecure();

        while (director.areAnyActionsLeft()) {
            List<StitchingDirector.StitchAction> stitchActions = director.getStitchActions();

            String stitchActionAuthString = "";
            for (StitchingDirector.StitchAction stitchAction : stitchActions)
                stitchActionAuthString += " " + stitchAction.getServer().getDefaultComponentManagerUrn();
            note("Stitch Actions needed " + stitchActions.size() + " on: " + stitchActionAuthString);
            LOG.info("Stitch Actions needed " + stitchActions.size() + " on: " + stitchActionAuthString);

            if (stitchActions.isEmpty())
                errorFatal("No stitch actions left: implementation bug.");

            for (StitchingDirector.StitchAction stitchAction : stitchActions) {
                Server auth = stitchAction.getServer();

                note("Handling StitchAction "+stitchAction.getType()+" on "+auth.getDefaultComponentManagerUrn());

                switch (stitchAction.getType()) {
                    case DELETE: {
                        AggregateManagerWrapper amWrapper = getAMWrapper(auth);

                        try {
                            note("Calling Delete on " + auth.getDefaultComponentManagerUrn() + ", as requested by stitching director.");
                            amWrapper.deleteSliver(sliceInfo.urn, sliceInfo.credentials);
                        } catch (JFedException e) {
                            //delete call can fail but still work, so we will ignore error
                            errorNonFatal("amWrapper.deleteSliver threw JFedException: "+e.getMessage()+" for "+e.getXmlRpcResult());
                        } finally {
                            toWait.remove(auth);
                            toProvision.remove(auth);
                            director.processDeleteResult(auth);
                        }
                        break;
                    }
                    case LISTRESOURCES:{
                        AggregateManagerWrapper amWrapper = getAMWrapper(auth);

                        String advertisementRspec;
                        try {
                            advertisementRspec = amWrapper.listResources(getUserCredentialList(), true/*available*/);
                            note("Called ListResources on " + auth.getDefaultComponentManagerUrn()+". " +
                                    "Got advertisement RSpec of "+(advertisementRspec == null ? "null" : advertisementRspec.length())+" chars");

                            director.processListResourcesResult(auth, advertisementRspec);
                        } catch (JFedException e) {
                            errorFatal("amWrapper.listResources threw JFedException: "+e.getMessage()+" for "+e.getXmlRpcResult());
                        }
                        break;
                    }
                    case ALLOCATE: {
                        String requestRspec = stitchAction.getRequestRspec();

                        note("   CreateSliver call for " + auth.getDefaultComponentManagerUrn());
                        LOG.info("   CreateSliver call for hop=" + stitchAction);
                        LOG.info("   CreateSliver call for auth=" + auth.getDefaultComponentManagerUrn());
                        NodeLoginTester nodeLoginTester =
                                new NodeLoginTester(this, fixedSshPublicKeyFile, fixedSshPrivateKeyFile, fixedSshPrivateKeyPassword);
                        nodeLoginTesters.put(auth, nodeLoginTester);

                        List users = new ArrayList<UserSpec>();
                        UserSpec userSpec =
                                new UserSpec(user.getUserUrnString(), nodeLoginTester.getSshKeyHelper().getSshPublicKeyString());
                        users.add(userSpec);

                        AggregateManagerWrapper amWrapper = getAMWrapper(auth);

                        String manifestRspec = null;
                        AbstractGeniAggregateManager.AggregateManagerReply reply;
                        try {
                            if (amWrapper.hasSeperateAllocateAndProvision()) {
                                amWrapper.allocate(sliceInfo.urn, sliceInfo.credentials, requestRspec, expirationDate);
                                reply = amWrapper.getLastReply();
                                manifestRspec = null;
                            } else {
                                manifestRspec = amWrapper.createSliver(sliceInfo.urn, sliceInfo.credentials, requestRspec, users, expirationDate);
                                reply = amWrapper.getLastReply();
                            }
                        } catch (JFedException e) {
                            //ignore
                            reply = (AbstractGeniAggregateManager.AggregateManagerReply) e.getSfaReply();
                        }

                        LOG.trace("   Processing result of CreateSliver call for " + auth.getDefaultComponentManagerUrn());
                        StitchingDirector.AllocateVerdict verdict = director.processAllocateResult(stitchAction, reply);
                        boolean failure = verdict == StitchingDirector.AllocateVerdict.GIVEUP || verdict == StitchingDirector.AllocateVerdict.CALLER_DELAYED_RETRY;
                        assertFalse(failure, "Stitching director reported failure for call with reply code=" + reply.getGeniResponseCode() + " output=" + reply.getOutput());

                        if (!failure)
                            note("Overview of hop data after CreateSliver call: \n" + director.getSuggestedAvailableOverviewsString(), true/*formatAsCode*/);

                        if (verdict == StitchingDirector.AllocateVerdict.OK) {
                            if (amWrapper.hasSeperateAllocateAndProvision()) {
                                toProvision.add(auth);
                            } else {
                                toWait.add(auth);

                                assert manifestRspec != null;
                                assertFalse(nodeLoginTester.hasParsed());
                                nodeLoginTester.parseSshInfoFromGeni3ManifestRspec(manifestRspec, auth, null);
                            }
                        }
                        break;
                    } //case ALLOCATE
                    default: {
                        errorFatal("Stitch Action "+stitchAction+" is not supported by this test.");
                    }
                } //switch
            } //for stitchActions
        } //while actions left


        //log VLAN's used
        try {
            Map<String, List<Hop>> hopOverview = director.getHopOverview();
            for (List<Hop> hops : hopOverview.values())
                for (Hop hop : hops) {
                    int authIndex = stitchedAuthorityUrns.indexOf(hop.getAuthUrn());
                    if (authIndex >= 0) {
                        //note: VLAN's this might get logged multiple times. But that's OK.
                        Integer vlan = hop.getConfirmedVlan();
                        commonAMTest.logParsableInfo("STITCH_AUTH"+authIndex+"_VLAN", vlan+"");
                    }
                }
        } catch (Exception t) {
            note("BUG: Exception while getting hop overview info (ignored because this is only informational data): "+t.getMessage());
            LOG.error("Ignored exception while processing hop overview.", t);
        }
    }

    @Test(hardDepends = {"doStitchActions"}, groups = {"createsliver"})
    public void provision() throws Throwable {
        for (Server auth : toProvision) {
            try {
                AggregateManagerWrapper amWrapper = getAMWrapper(auth);

                NodeLoginTester nodeLoginTester = nodeLoginTesters.get(auth);
                List userSpecs = new ArrayList<UserSpec>();
                UserSpec userSpec =
                        new UserSpec(user.getUserUrnString(), nodeLoginTester.getSshKeyHelper().getSshPublicKeyString());
                userSpecs.add(userSpec);

                String manifestRspec = null;
                AbstractGeniAggregateManager.AggregateManagerReply reply;
                try {
                    manifestRspec = amWrapper.provisionAndStart(sliceInfo.urn, sliceInfo.credentials, userSpecs, expirationDate);
                    note("Called Provision on " + auth.getDefaultComponentManagerUrn());
                    reply = amWrapper.getLastReply();
                } catch (JFedException e) {
                    //ignore
                    note("amWrapper.provisionAndStart threw JFedException: "+e.getMessage()+" for "+e.getXmlRpcResult());
                    reply = (AbstractGeniAggregateManager.AggregateManagerReply) e.getSfaReply();
                }

                if (reply.getGeniResponseCode().isSuccess()) {
                    assert manifestRspec != null;

                    assert nodeLoginTester != null;
                    if (nodeLoginTester != null) {
                        boolean parseFoundLoginInfo = nodeLoginTester.parseSshInfoFromGeni3ManifestRspec(manifestRspec, auth, null);
                        if (!parseFoundLoginInfo)
                            note("Found no SSH login info in manifest received at " + auth);
                    }
                }
            } catch (Exception e) {
                errorNonFatal("Error while calling Provision and PerformOperationalAction on " + auth.getDefaultComponentManagerUrn() + ".");
                throw e;
            }

            toWait.add(auth);
        }

        toProvision.clear();
    }

    @Test(hardDepends = {"provision", "doStitchActions"}, groups = {"createsliver"})
    public void waitForAllReady() throws JFedException, URISyntaxException {
        long now = System.currentTimeMillis();
        long deadline = now + (20 * 60 * 1000);

        assertTrue(toWait.size() > 0, "There are no authorities to wait for. (Actively involved count = "+director.getActivelyInvolvedAuthorities()+")");

        //Test if the slivers ever becomes ready. We wait for maximum 20 minutes.
        while (now < deadline) {
            List<Server> sliverAuthsToCheck = new ArrayList<>(toWait);
            assertTrue(sliverAuthsToCheck.size() > 0);

            for (Server auth : sliverAuthsToCheck) {
                now = System.currentTimeMillis();
                LOG.info("   Calling SliverStatus on " + auth.getDefaultComponentManagerUrn() + ". Deadline in: " + (deadline - now) + " ms");

                AggregateManagerWrapper amWrapper = getAMWrapper(auth);

                StatusDetails statusDetails = null;
                AbstractGeniAggregateManager.AggregateManagerReply reply;
                try {
                    statusDetails = amWrapper.status(sliceInfo.urn, sliceInfo.credentials);
                    reply = amWrapper.getLastReply();
                } catch (JFedException e) {
                    //ignore
                    note("amWrapper.status threw JFedException: "+e.getMessage()+" for "+e.getXmlRpcResult());
                    reply = (AbstractGeniAggregateManager.AggregateManagerReply) e.getSfaReply();
                }

                if (reply == null)
                    errorFatal("sliver status call failed (reply == null) " + " @ " + auth.getDefaultComponentManagerUrn());
                if (!reply.getGeniResponseCode().isSuccess())
                    errorFatal("sliver status call failed (code="+reply.getGeniResponseCode()+")" + " @ " + auth.getDefaultComponentManagerUrn());
                if (statusDetails == null)
                    errorFatal("sliver status call failed (returned StatusDetails == null) " + " @ " + auth.getDefaultComponentManagerUrn());

                if (Objects.equals(statusDetails.getGlobalStatus(), StatusDetails.SliverStatus.FAIL)) {
                    errorNonFatal("testCreatedSliverBecomesReady -> sliver failed instead of becoming ready: " + statusDetails.getGlobalStatus() + "" + " @ " + auth.getDefaultComponentManagerUrn());
                    errorFatal("sliver failed " + " @ " + auth.getDefaultComponentManagerUrn());
                }


                if (Objects.equals(statusDetails.getGlobalStatus(), StatusDetails.SliverStatus.READY)) {
                    note("testCreatedSliverBecomesReady -> sliver ready: " + statusDetails.getGlobalStatus() + " @ " + auth.getDefaultComponentManagerUrn());
                    toWait.remove(auth);
                    toDescribe.add(auth);
                    if (toWait.isEmpty()) {
                        note("testCreatedSliverBecomesReady -> all slivers ready");
                        return;
                    }
                    continue;
                }

                note("testCreatedSliverBecomesReady -> sliver not ready: " + statusDetails.getGlobalStatus() + "" + " @ " + auth.getDefaultComponentManagerUrn());
                LOG.info("      Sliver not ready: \"" + statusDetails.getGlobalStatus() + "\". Deadline in: " + (deadline - now) + " ms");

            }

            now = System.currentTimeMillis();
            note("testCreatedSliverBecomesReady -> At least one sliver still not ready.  Trying again in 30 seconds...");
            LOG.info("   At least one sliver still not ready. Sleeping for 30 seconds. Deadline in: " + (deadline - now) + " ms");

            try {
                Thread.sleep(30000 /*ms*/);
            } catch (InterruptedException e) { /* Ignore*/ }

            now = System.currentTimeMillis();
        }
        errorFatal("Some slivers did not become ready within 20 minutes: " + toWait);
    }

    @Test(hardDepends = {"waitForAllReady"}, groups = {"createsliver", "nodelogin"})
    public void describeAllResources() throws JFedException {
        for (Server auth : toDescribe) {
            if (isNodeAuth(auth))
                try {
                    AggregateManagerWrapper amWrapper = getAMWrapper(auth);

                    String manifestRspec = null;
                    AbstractGeniAggregateManager.AggregateManagerReply reply;
                    try {
                        manifestRspec = amWrapper.describe(sliceInfo.urn, sliceInfo.credentials);
                        note("Called describe on " + auth.getDefaultComponentManagerUrn());
                        reply = amWrapper.getLastReply();
                    } catch (JFedException e) {
                        //ignore
                        note("amWrapper.describe threw JFedException: "+e.getMessage()+" for "+e.getXmlRpcResult(), true/*formatAsCode*/);
                        reply = (AbstractGeniAggregateManager.AggregateManagerReply) e.getSfaReply();
                    }

                    if (reply.getGeniResponseCode().isSuccess()) {
                        assert manifestRspec != null;

                        NodeLoginTester nodeLoginTester = nodeLoginTesters.get(auth);
                        assert nodeLoginTester != null;
                        if (nodeLoginTester != null) {
                            boolean parseFoundLoginInfo = nodeLoginTester.parseSshInfoFromGeni3ManifestRspec(manifestRspec, auth, null);
                            if (!parseFoundLoginInfo)
                                warn("Found no SSH login info in manifest received at "+auth);
                        }
                    }
                } catch (Exception e) {
                    errorNonFatal("Error while calling describe on "+ auth.getDefaultComponentManagerUrn()+". Will continue with others.");
                }
        }
    }

    @Test(hardDepends = {"waitForAllReady"}, softDepends = {"describeAllResources"}, groups = {"nodelogin"})
    public void loginAndPing() throws JFedException, IOException {
        super.loginAndPing();
    }


    @Test(softDepends = {"loginAndPing", "doStitchActions", "waitForAllReady", "describeAllResources"}, hardDepends = {
            "createSlice"}, groups = {"createsliver", "nodelogin"})
    public void callDeletes() throws JFedException {
        super.callDeletes();
    }
}
