package be.iminds.ilabt.jfed.highlevel.jobs;

import be.iminds.ilabt.jfed.MockHttpRepliesTestCoreModule;
import be.iminds.ilabt.jfed.TestAuthoritiesModule;
import be.iminds.ilabt.jfed.TestUserModule;
import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetails;
import be.iminds.ilabt.jfed.experiment.Experiment;
import be.iminds.ilabt.jfed.experiment.tasks.ExperimentTaskStatus;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.highlevel.jobs.report.JobStateReport;
import be.iminds.ilabt.jfed.highlevel.jobs.report.StitchingJobReport;
import be.iminds.ilabt.jfed.highlevel.jobs.report.StitchingJobReportWriter;
import be.iminds.ilabt.jfed.highlevel.jobs.states.JobStateFactory;
import be.iminds.ilabt.jfed.highlevel.model.SfaModel;
import be.iminds.ilabt.jfed.highlevel.model.Slice;
import be.iminds.ilabt.jfed.highlevel.stitcher.*;
import be.iminds.ilabt.jfed.log.cache.ApiCallDetailsCache;
import be.iminds.ilabt.jfed.lowlevel.AnyCredential;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.TestCertHelper;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.connection.MockReplyLibrary;
import be.iminds.ilabt.jfed.lowlevel.connection.MockSfaConnection;
import be.iminds.ilabt.jfed.lowlevel.stitching.VlanRange;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.rspec.model.ModelRspecType;
import be.iminds.ilabt.jfed.rspec.rspec_source.RequestRspecSource;
import be.iminds.ilabt.jfed.util.GeniUrn;
import com.google.inject.Guice;
import com.google.inject.Injector;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import org.apache.xmlrpc.XmlRpcRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.io.File;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.testng.Assert.*;

/**
 * User: twalcari
 * Date: 12/9/2015
 * Time: 11:25
 */
public class StartStitchedExperimentJobSetup {
    private static final Logger LOG = LoggerFactory.getLogger(StartStitchedExperimentJobSetup.class);

    private final StitchingTestSetupConfig conf;

    @Inject
    private JFedPreferences jFedPreferences;
    @Inject
    private TestbedInfoSource testbedInfoSource;
    @Inject
    private GeniUserProvider geniUserProvider;
    @Inject
    private SfaModel sfaModel;
    @Inject
    private JobFactory jobFactory;
    @Inject
    private ApiCallDetailsCache cache;
    @Inject
    private TestStitchingXmlRpcHelper testStitchingXmlRpcHelper;
    @Inject
    private TestCertHelper testCertHelper;


    private MockHttpRepliesTestCoreModule testCoreModule;

    private TestStitchingAMServer mockAm1;
    private TestStitchingAMServer mockAm2;
    private TestStitchingAMServer mockAm3;


    private String advertisedAvailableAm1 = "197,300-305,750-755";
    private String advertisedAvailableAm2 = "197,300-305,750-755";
    private String advertisedAvailableAm3 = "197,300-305,750-755";
    String realAvailableAm1;
    private String realAvailableAm2;
    private String realAvailableAm3 = "197,304-305,750-755";


    public Server testAm1Auth;
    public Server testAm2Auth;
    public Server testScsAuth;

    private Slice slice;
    private Experiment experiment;


    public StartStitchedExperimentJobSetup(StitchingTestSetupConfig conf) {
        this.conf = conf;


        testCoreModule = new MockHttpRepliesTestCoreModule();

        Injector injector = Guice.createInjector(testCoreModule,
                new TestUserModule(),
                new StitchingTestModule(),
                new StitchedTestAuthoritiesModule(conf));

        injector.injectMembers(this);


        if (conf.hasSpecialCase(StitchingTestSetupConfig.SpecialCase.am1NoMatchingVlansAm2)) {
            realAvailableAm1 = "300-302,753-755";     //count=6
            realAvailableAm2 = "197,304-305,751-752"; //count=5
        } else {
            realAvailableAm1 = "300-302,305,750-755";
            realAvailableAm2 = "197,304-305,750-755";
        }


        //Create replies library
        MockReplyLibrary mockReplyLibrary = new MockReplyLibrary();
        testCoreModule.addMockReply(mockReplyLibrary.getVersion3());
        testCoreModule.addMockReply(new MockSfaConnection.AbstractMockXmlRpcReply() {
            @Override
            public MockSfaConnection.MockXmlRpcAnswer matches(XmlRpcRequest request) {
                if (!Objects.equals(MockReplyLibrary.getServerUrl(request), TestAuthoritiesModule.urlC_scs))
                    return null;
                if (Objects.equals(request.getMethodName(), "ComputePath") && request.getParameterCount() > 0) {
                    if (conf.hasSpecialCase(StitchingTestSetupConfig.SpecialCase.scsFailure))
                        return new MockSfaConnection.MockXmlRpcAnswer(testStitchingXmlRpcHelper.wrapErrorReply(5, "SCS failure"));

                    if (conf.amCount() < 3 || conf.am(3).noStitching) {
                        if (conf.hasSpecialCase(StitchingTestSetupConfig.SpecialCase.am2DependsOnAm1))
                            return new MockSfaConnection.MockXmlRpcAnswer(testStitchingXmlRpcHelper.create2DepAmScsHttpReply(
                                    advertisedAvailableAm1, advertisedAvailableAm2,
                                    "304", "304", conf.hasSpecialCase(StitchingTestSetupConfig.SpecialCase.hasFakeAm3),
                                    conf.amCount() > 2 && conf.am(3).noStitching));
                        else
                            return new MockSfaConnection.MockXmlRpcAnswer(testStitchingXmlRpcHelper.create2IndepAmScsHttpReply(
                                    advertisedAvailableAm1, advertisedAvailableAm2,
                                    "304", "304", conf.hasSpecialCase(StitchingTestSetupConfig.SpecialCase.hasFakeAm3),
                                    conf.amCount() > 2 && conf.am(3).noStitching));
                    } else {
                        //3 or more am's
                        assert conf.amCount() == 3;
                        assert conf.hasSpecialCase(StitchingTestSetupConfig.SpecialCase.am2DependsOnAm1);
                        return new MockSfaConnection.MockXmlRpcAnswer(testStitchingXmlRpcHelper.create3DepAmScsHttpReply(
                                advertisedAvailableAm1, advertisedAvailableAm2, advertisedAvailableAm3,
                                "304", "304", "304"));
                    }
                }
                return null;
            }
        });

        if (conf.am(1).hasV2) {
            mockAm1 =
                    new TestStitchingAMv2Server(TestAuthoritiesModule.urlA_am2, TestCertHelper.am1Urn, TestCertHelper.authority1Hostname,
                            "stitchIface1", new VlanRange(realAvailableAm1), conf.am(1), null);
            testCoreModule.addMockReply(mockAm1);
        }
        if (conf.am(1).hasV3) {
            mockAm1 =
                    new TestStitchingAMv3Server(TestAuthoritiesModule.urlA_am3, TestCertHelper.am1Urn, TestCertHelper.authority1Hostname,
                            "stitchIface1", new VlanRange(realAvailableAm1), conf.am(1), null);
            testCoreModule.addMockReply(mockAm1);
        }

        if (conf.am(2).hasV2) {
            mockAm2 =
                    new TestStitchingAMv2Server(TestAuthoritiesModule.urlB_am2, TestCertHelper.am2Urn, TestCertHelper.authority2Hostname,
                            "stitchIface2", new VlanRange(realAvailableAm2), conf.am(2),
                            conf.hasSpecialCase(StitchingTestSetupConfig.SpecialCase.am2DependsOnAm1) ? mockAm1 : null);
            testCoreModule.addMockReply(mockAm2);
        }
        if (conf.am(2).hasV3) {
            mockAm2 =
                    new TestStitchingAMv3Server(TestAuthoritiesModule.urlB_am3, TestCertHelper.am2Urn, TestCertHelper.authority2Hostname,
                            "stitchIface2", new VlanRange(realAvailableAm2), conf.am(2),
                            conf.hasSpecialCase(StitchingTestSetupConfig.SpecialCase.am2DependsOnAm1) ? mockAm1 : null);
            testCoreModule.addMockReply(mockAm2);
        }

        if (conf.amCount() >= 3) {
            if (conf.am(3).hasV2) {
                mockAm3 =
                        new TestStitchingAMv2Server(TestAuthoritiesModule.urlC_am2, TestCertHelper.am3Urn, TestCertHelper.authority3Hostname,
                                "stitchIface3", new VlanRange(realAvailableAm3), conf.am(3), null/*dependency*/);
                testCoreModule.addMockReply(mockAm3);
            }
            if (conf.am(3).hasV3) {
                mockAm3 =
                        new TestStitchingAMv3Server(TestAuthoritiesModule.urlC_am3, TestCertHelper.am3Urn, TestCertHelper.authority3Hostname,
                                "stitchIface3", new VlanRange(realAvailableAm3), conf.am(3), null/*dependency*/);
                testCoreModule.addMockReply(mockAm3);
            }
        }

        TestStitchingSAServer mockSa =
                new TestStitchingSAServer(TestAuthoritiesModule.urlA_pgSa1, TestCertHelper.saUrn, TestCertHelper.authority1Hostname, testCertHelper);
        testCoreModule.addMockReply(mockSa);


        testAm1Auth = testbedInfoSource.getByUrnExact(TestCertHelper.am1Urn);
        testAm2Auth = testbedInfoSource.getByUrnExact(TestCertHelper.am2Urn);
        testScsAuth = testbedInfoSource.getByUrnExact(TestCertHelper.scsUrn);

        mockAm1.setAuthority(testAm1Auth);
        mockAm2.setAuthority(testAm2Auth);


        //setup the Slice and Experiment
        slice = mock(Slice.class);

        GeniUrn sliceUrn = GeniUrn.createGeniUrnFromEncodedParts(TestCertHelper.authority1Hostname, "slice", "test");
        when(slice.getUrn()).thenReturn(sliceUrn);
        when(slice.getUrnString()).thenReturn(sliceUrn + "");


        Instant expirationDate = Instant.now().plusSeconds(2 * 60 * 60);
        when(slice.getExpirationDate()).thenReturn(expirationDate);
        when(slice.expirationDateProperty()).thenReturn(new SimpleObjectProperty<>());

        RequestRspecSource requestRspecSource =
                new RequestRspecSource(testStitchingXmlRpcHelper.createStitchRequest(), ModelRspecType.BASIC);
        when(slice.getRequestRspec()).thenReturn(requestRspecSource);
        when(slice.requestRspecProperty()).thenReturn(new SimpleObjectProperty<>());

        when(slice.hasCredential()).thenReturn(true);

        when(slice.getSlivers()).thenReturn(FXCollections.emptyObservableList());

        experiment = new Experiment("test", null, requestRspecSource, null, expirationDate, null, false);
        experiment.registerSlice(slice);
    }

    private AllocateExperimentJob allocateExperimentJob;



    public void testStitching() throws Exception {

        allocateExperimentJob = jobFactory.createAllocateExperimentJob(experiment, testScsAuth);


        //fake user credentials
        AnyCredential userCredential = mock(AnyCredential.class);
        when(userCredential.getCredentialXml()).thenReturn("fake credential for testing");

        Map<String, Object> userCredentialV3Ht = new HashMap<>();
        userCredentialV3Ht.put("geni_type", "geni");
        userCredentialV3Ht.put("geni_version", "3");
        userCredentialV3Ht.put("geni_value", "fake credential for testing");
        when(userCredential.getGeniV3Map()).thenReturn(userCredentialV3Ht);
        List<AnyCredential> userCredentials = new ArrayList<>();
        userCredentials.add(userCredential);


        System.out.flush();
        System.err.flush();

        System.out.println("Parallel stitcher started with slice " + slice.getUrn());
        System.out.println("\n\n\n");

        allocateExperimentJob.call();

        System.out.flush();
        System.err.flush();
        System.out.println("\n\n\n");

        System.out.println("Call history:");
        int i = 0;

        for (MockSfaConnection.MockXmlRpcHistoryItem histItem : testCoreModule.getMockXmlRpcHistory()) {
            System.out.println("Call " + i + ": " + histItem.request.getMethodName() + " @ " + MockReplyLibrary.getServerUrl(histItem.request));

            i++;
        }
        System.out.println("");
        for (XmlRpcRequest request : testCoreModule.getNoMockXmlRpcRequestInfo()) {
            System.out.println("No Mock reply for: " + request.getMethodName() + " @ " + MockReplyLibrary.getServerUrl(request));
        }

        System.out.println("\n\n\n");
        System.out.println("Test finished in with parallelStitcher.getGlobalPhase()=" + allocateExperimentJob.getState().getName());

        System.out.flush();
        System.err.flush();
    }

    public void outputToFile() {
        String basename = "stitching-";
        if (conf.am(1).hasV2) basename += "2";
        if (conf.am(1).hasV3) basename += "3";
        basename += "-";
        if (conf.am(2).hasV2) basename += "2";
        if (conf.am(2).hasV3) basename += "3";
        if (conf.amCount() >= 3) {
            basename += "-";
            if (conf.am(3).hasV2) basename += "2";
            if (conf.am(3).hasV3) basename += "3";
        }
        basename += "-";
        basename += conf.am(1).noStitching ? "nostitch" : (conf.am(1).supportsAny ? "any" : "noany");
        basename += "-";
        basename += conf.am(2).noStitching ? "nostitch" : (conf.am(2).supportsAny ? "any" : "noany");
        if (conf.amCount() >= 3) {
            basename += "-";
            basename += conf.am(3).noStitching ? "nostitch" : (conf.am(3).supportsAny ? "any" : "noany");
        }

        for (StitchingTestSetupConfig.SpecialCase specialCase : conf.specialCases)
            switch (specialCase) {
                case am2DependsOnAm1: {
                    basename += "-2deps1";
                    break;
                }
                case am1NoMatchingVlansAm2: {
                    basename += "-am1noSameVlanAm2";
                    break;
                }
                case hasFakeAm3: {
                    basename += "-fake";
                    break;
                }
                default: {
                    basename += "-" + specialCase;
                    break;
                }
            }

        for (StitchingTestSetupConfig.SpecialReply specialReply : conf.am(1).specialReplies)
            basename += "-1s" + specialReply.ordinal();
        for (StitchingTestSetupConfig.SpecialReply specialReply : conf.am(2).specialReplies)
            basename += "-2s" + specialReply.ordinal();
        if (conf.amCount() >= 3)
            for (StitchingTestSetupConfig.SpecialReply specialReply : conf.am(3).specialReplies)
                basename += "-3s" + specialReply.ordinal();

        this.outputToFile(basename);
    }

    public void outputToFile(String basename) {
        File file = new File(basename + ".html");
        new StitchingJobReportWriter(allocateExperimentJob.getStitchingJobReport(), cache).toHtmlFile(file, true);
        System.out.println("Wrote " + basename + ".html");
        System.out.println("      " + file.getAbsolutePath());
        System.out.println("      " + file.toURI());
        System.out.flush();
        System.err.flush();

    }

    public void assertAllTestBasics() {
        testCoreModule.getMockXmlRpcHistory().stream()
                .filter(histItem -> !histItem.reply.isException())
                .forEach(histItem ->
                        assertThat("A request was seen by the fake server as a test failure",
                                histItem.reply.getString(),
                                not(containsString("TEST BUG DETECTED"))));

        //basic checks, even for failed stitchings, these should succeed
        assertThat("One or more GetStatusUntilReadyOrFail states did not return successful", !hasTimedOutStatusJobs());
        assertThat("Encountered a request for which no answer was mocked: ", testCoreModule.getNoMockXmlRpcRequestInfo(), empty());
    }



    public void assertMostTestBasics() {
        assertAllTestBasics();

        JobStateReport allocateStateReport = getStitchingJobReport().getJobStateReports().stream()
                .filter(jobStateReport -> jobStateReport.getStateName().startsWith("Allocate"))
                .findFirst().orElse(null);
        assertNotNull(allocateStateReport);

        List<SerializableApiCallDetails> allocateStateCalls =
                allocateStateReport.getTaskExecutions().stream()
                        .flatMap(taskExecution -> taskExecution.getApiCallHistory().stream())
                        .map(cache::getApiCallDetails)
                        .collect(Collectors.toList());

        List<SerializableApiCallDetails> deleteCalls = allocateStateCalls.stream().
                filter(c -> c.getGeniMethodName().startsWith("Delete")).collect(Collectors.toList());
        List<SerializableApiCallDetails> allocateCalls = allocateStateCalls.stream().filter(
                c -> c.getGeniMethodName().equals("Allocate") || c.getGeniMethodName().equals("CreateSliver")).collect(Collectors.toList());
        List<SerializableApiCallDetails> allocateStateStatusCalls = allocateStateCalls.stream().filter(
                c -> c.getGeniMethodName().contains("Status")).collect(Collectors.toList());
        assertThat("there should be at least one Allocate in the Allocate state",
                allocateCalls, hasSize(greaterThan(0)));
        assertThat("there should be at least one Allocate for every Delete",
                allocateCalls, hasSize(greaterThanOrEqualTo(deleteCalls.size())));

        boolean hasAnyAmv2 = conf.am(1).hasV2 || conf.am(2).hasV2;
        if (conf.amCount() >= 3) hasAnyAmv2 |= conf.am(3).hasV2;
        if (!hasAnyAmv2)
            assertThat("there should be no Status calls in the Allocate state if there are no AMv2 AMs",
                    allocateStateStatusCalls, hasSize(is(0)));

        //am1 does not want expires, am2 and scs do
        for (SerializableApiCallDetails allocateDetail : allocateCalls) {
            Server auth = testbedInfoSource.getByUrnExact(allocateDetail.getAuthorityUrn());
            boolean addExpires = auth.hasFlag(Server.Flag.workaroundAddExpiresAttributeToRequestRspec);

            //check if the ADD_EXPIRES_ATTRIBUTE_TO_REQUEST is correctly registered in the Server
            if (Objects.equals(allocateDetail.getAuthorityUrn(), TestCertHelper.am1Urn))
                assertThat(addExpires, is(true));
            if (Objects.equals(allocateDetail.getAuthorityUrn(), TestCertHelper.am2Urn))
                assertThat(addExpires, is(false));
            if (Objects.equals(allocateDetail.getAuthorityUrn(), TestCertHelper.scsUrn))
                assertThat(addExpires, is(true));

            String requestRspec =
                    allocateDetail.getXmlRpcRequestJsonString(); //don't care that this is more than the RSpec, it's good enough for this test.
            boolean hasExpires = requestRspec.contains("expires=");
//            LOG.debug("Testing if "+auth.getUrnTld()+" has expires. request RSpec = "+requestRspec);
            if (addExpires)
                assertThat(allocateDetail.getAuthorityUrn() + " should have expires in request RSpec", hasExpires, is(true));
            else
                assertThat(allocateDetail.getAuthorityUrn() + " should not have expires in request RSpec", hasExpires, is(false));
        }
    }

    public void assertSuccess(boolean allowDelete) {
        assertMostTestBasics();
        assertTrue(allocateExperimentJob.isSuccess(), "Gave up on stitching. globalPhase is " + allocateExperimentJob.getState().getName());
        //assertThat(allocateExperimentJob.getState(), instanceOf(AllocateExperimentJob.UpdateSfaPartsUntilReadyState.class));
        assertThat(allocateExperimentJob.getState().getStatus(), equalTo(ExperimentTaskStatus.SUCCESS));

        if (conf.hasSpecialCase(StitchingTestSetupConfig.SpecialCase.am2DependsOnAm1)) {
            assertNotNull(mockAm1.getActiveVlan());
            assertNotNull(mockAm2.getActiveVlan());
            assertThat(mockAm2.getActiveVlan(), equalTo(mockAm1.getActiveVlan()));
        }

        if (!allowDelete)
            for (MockSfaConnection.MockXmlRpcHistoryItem histItem : testCoreModule.getMockXmlRpcHistory()) {
                assertThat(histItem.request.getMethodName(), not(containsString("Delete")));
            }

        System.out.flush();
        System.err.flush();
    }

    public void assertFailure() {
        assertMostTestBasics();
        assertFalse(allocateExperimentJob.isSuccess(), "Did not give up on stitching. globalPhase is " + allocateExperimentJob.getState().getName());
        assertThat(allocateExperimentJob.getState(), instanceOf(JobStateFactory.StopExperimentState.class));

        System.out.flush();
        System.err.flush();
    }


    public StitchingJobReport getStitchingJobReport() {
        return allocateExperimentJob.getStitchingJobReport();
    }

    public AllocateExperimentJob getAllocateExperimentJob() {
        return allocateExperimentJob;
    }

    public boolean isGaveUpOnStitching() {
        return allocateExperimentJob.getStitchingJobReport().getJobStateReports().stream()
                .filter(jobStateReport -> Objects.equals(jobStateReport.getStateName(), "Request stitched link setup-instructions"))
                .findFirst().map(JobStateReport::getFinalStatus).orElse(ExperimentTaskStatus.FAILED)
                == ExperimentTaskStatus.FAILED;
    }

    public boolean hasTimedOutStatusJobs(){

        //TODO: reimplement!
        return false;
    }


    public ApiCallDetailsCache getApiCallDetailsCache() {
        return cache;
    }


}
