package be.iminds.ilabt.jfed.testing.base;

import be.iminds.ilabt.jfed.call_log_output.HtmlLogOutput;
import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetails;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.util.TargetAuthority;
import org.rendersnake.HtmlCanvas;
import org.slf4j.LoggerFactory;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.*;

import static org.rendersnake.HtmlAttributesFactory.class_;

/**
 * ApiTestResult: list of test method results
 */
public class ApiTestResult extends HtmlLogOutput {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(ApiTestResult.class);

    protected final TargetAuthority testedAuthority;
    protected final GeniUser user;


    @XmlRootElement(name = "method")
    public static class ApiTestMethodResult {
        String methodName;
        String description;
        List<String> hardDependencies;
        List<String> softDependencies;

        List<LogEntry> logLines = new ArrayList<>();
        List<SerializableApiCallDetails> apiCallDetails = new ArrayList<>();
        Throwable exception;

        protected TestResultState state;

        long timeMs;
        long startTimeMs;
        long stopTimeMs;

        public ApiTestMethodResult() {
        }

        public ApiTestMethodResult(String methodName, String description, List<String> hardDependencies, List<String> softDependencies, List<LogEntry> logLines, List<SerializableApiCallDetails> apiCallDetails, Throwable exception, TestResultState state, long timeMs, long startTimeMs, long stopTimeMs) {
            this.methodName = methodName;
            this.description = description;
            this.hardDependencies = hardDependencies;
            this.softDependencies = softDependencies;
            this.logLines = logLines;
            this.apiCallDetails = apiCallDetails;
            this.exception = exception;
            this.state = state;
            this.timeMs = timeMs;
            this.startTimeMs = startTimeMs;
            this.stopTimeMs = stopTimeMs;
        }

        public void addLogLine(LogLineType type, String text, Throwable t, boolean formatAsCode) {
            assert type != null;
            if (text == null)
                text = "";
            assert text != null;
            logLines.add(new LogEntry(type, text, t, formatAsCode));
        }
        public void addLogLine(LogLineType type, String text, Throwable t) {
            assert type != null;
            if (text == null)
                text = "";
            assert text != null;
            logLines.add(new LogEntry(type, text, t));
        }
        public void addLogLine(LogLineType type, String text, boolean formatAsCode) {
            assert type != null;
            if (text == null)
                text = "";
            assert text != null;
            logLines.add(new LogEntry(type, text, formatAsCode));
        }
        public void addLogLine(LogLineType type, String text) {
            assert type != null;
            if (text == null)
                text = "";
            assert text != null;
            logLines.add(new LogEntry(type, text));
        }
        public void addLogLine(LogEntry logEntry) {
            logLines.add(logEntry);
        }

        @XmlElement(name = "state")
        public TestResultState getState() {
            return state;
        }

        @XmlElement(name = "methodName")
        public String getMethodName() {
            return methodName;
        }

        @XmlElement(name = "description")
        public String getDescription() {
            return description;
        }

        public List<String> getHardDependencies() {
            return Collections.unmodifiableList(hardDependencies);
        }

        public List<String> getSoftDependencies() {
            return Collections.unmodifiableList(softDependencies);
        }

        @XmlElementWrapper(name = "loglines")
        @XmlElement(name = "logline")
        public List<LogEntry> getLogLines() {
            return Collections.unmodifiableList(logLines);
        }

        @XmlElementWrapper(name = "apicalls")
        @XmlElement(name = "apicall")
        public List<SerializableApiCallDetails> getApiCallDetails() {
            return Collections.unmodifiableList(apiCallDetails);
        }

        public Throwable getException() {
            return exception;
        }

        @XmlElement(name = "durationInMs")
        public long getTimeMs() {
            return timeMs;
        }

        @XmlElement(name = "startTimeInMs")
        public long getStartTimeMs() {
            return startTimeMs;
        }

        @XmlElement(name = "stopTimeInMs")
        public long getStopTimeMs() {
            return stopTimeMs;
        }

        @Override
        public String toString() {
            return "ApiTestMethodResult{" +
                    "methodName='" + methodName + '\'' +
                    ", description='" + description + '\'' +
                    ", hardDependencies=" + hardDependencies +
                    ", softDependencies=" + softDependencies +
                    ", logLines=" + logLines +
                    ", apiCallDetails=" + apiCallDetails +
                    ", exception=" + exception +
                    ", state=" + state +
                    ", timeMs=" + timeMs +
                    ", startTimeMs=" + startTimeMs +
                    ", stopTimeMs=" + stopTimeMs +
                    '}';
        }
    }


    private final Class<? extends ApiTest> testClass;
    private final String testGroup;

    public ApiTestResult(TargetAuthority testedAuthority, GeniUser user, Class<? extends ApiTest> testClass, String testGroup) {
        super("Test Result");
        this.testedAuthority = testedAuthority;
        this.user = user;

        this.testClass = testClass;
        this.testGroup = testGroup;
    }

    public Class<? extends ApiTest> getTestClass() {
        return testClass;
    }

    /**
     * ordered list of results
     */
    final List<ApiTestMethodResult> resultList = new ArrayList<>();
    int returnValue = 0;

    List<ApiTestMethodResult> getResultList() {
        return Collections.unmodifiableList(resultList);
    }

    public Date getStartDate() {
        Date start = null;
        for (ApiTestMethodResult result : resultList) {
            Date date = new Date(result.getStartTimeMs());
            if (start == null || start.after(date))
                start = date;
        }
        return start;
    }

    public Date getStopDate() {
        Date stop = null;
        for (ApiTestMethodResult result : resultList) {
            Date date = new Date(result.getStopTimeMs());
            if (stop == null || stop.before(date))
                stop = date;
        }
        return stop;
    }

    public long getDurationMs() {
        Date begin = getStartDate();
        Date end = getStopDate();
        if (begin == null || end == null)
            return 0;
        return end.getTime() - begin.getTime();
    }

    public String getVersionString() {
        StringBuilder version = new StringBuilder();
        try {
            final String VERSION_PROPERTIES_FILE = "/version.properties";
            final String PROP_VERSION = "version";
            final String PROP_GIT_COMMIT = "build.git_commit";
            final String PROP_GIT_BRANCH = "build.git_branch";
            final String PROP_BUILD_JENKINS = "build.jenkins_build";

            Properties prop = new Properties();
            //now load the version-properties file
            prop.load(getClass().getResourceAsStream(VERSION_PROPERTIES_FILE));

            //now load the info into the disabled textfield

            version.append(prop.getProperty(PROP_VERSION, "Unknown"));
            final String jenkinsBuildNr = prop.getProperty(PROP_BUILD_JENKINS);
            if (jenkinsBuildNr != null && !jenkinsBuildNr.isEmpty())
                version.append("  - build #").append(jenkinsBuildNr);

            final String gitCommit = prop.getProperty(PROP_GIT_COMMIT);
            if (gitCommit != null && !gitCommit.isEmpty())
                version.append("  - git commit #").append(gitCommit);

            final String gitBranch = prop.getProperty(PROP_GIT_BRANCH);
            if (gitBranch != null && !gitBranch.isEmpty())
                version.append(" on ").append(gitBranch);
            System.out.println("Version: "+version.toString());
        } catch (Exception e) {
            //ignore
            version.append(" === EXCEPTION finding version: ").append(e.getMessage()).append("\"");
            LOG.warn("EXCEPTION finding version: \""+e.getMessage()+"\"", e);
        }
        return version.toString();
    }

    public String getEnvironment() {
        String environment = System.getProperty("os.name") +
                " " +
                System.getProperty("os.version") +
                " " +
                System.getProperty("os.arch") +
                " - Java " +
                System.getProperty("java.version") +
                " (" +
                System.getProperty("java.vendor") +
                ")";
        return environment;
    }
    /*
     * return value meaning:
     * 2: errors
     * 1: warnings but no errors
     * 0: all success
     */
    public int getReturnValue() {
        return returnValue;
    }


    @XmlRootElement(name = "overview")
    private static class XmlTestOverview {
        @XmlElementWrapper(name = "methods")
        @XmlElement(name = "method")
        private List<ApiTestMethodResult> details;

        @XmlElement(name = "version") private String version;
        @XmlElement(name = "environment") private String environment;

        @XmlElement(name = "TestUserUrn") private String testUserUrn;
        @XmlElement(name = "TestUserAuthorityUrn") private String testUserAuhUrn;
        @XmlElement(name = "TestedAuthorityUrn") private String testedAuthUrn;
        @XmlElement(name = "TestedClass") private String testedClass;
        @XmlElement(name = "TestedMethods") private String testedMethods;

        public XmlTestOverview() {
        }

        public XmlTestOverview(List<ApiTestMethodResult> resultList, String version, String environment) {
            this.version = version;
            this.environment = environment;
            details = new ArrayList<>(resultList);
        }
    }

    /**
     * write an XML file with an overview. This uses JAXB.
     * <p/>
     * Exceptions and API call details are not included: this requires adding a lot more JAXB annotations/code.
     * Only the method details are currently included, no general information about the test. To add that, don't use XmlTestOverview but annotate this class directly instead.
     */
    public void toXmlOverview(File file) {
        toXmlOverview(file, false);
    }

    public void toXmlOverview(File file, boolean showCredentials) {
        disableRemoveSecurityRisks = showCredentials; //a bit of a hack now, would be nice to do this cleaner

        try {
            XmlTestOverview x = new XmlTestOverview(resultList, getVersionString(), getEnvironment());
            x.testUserUrn = user.getUserUrnString();
            x.testUserAuhUrn = user.getUserAuthorityServer().getDefaultComponentManagerUrn();
            x.testedAuthUrn = testedAuthority.getServerToConnect() == null ? "" : testedAuthority.getServerToConnect().getDefaultComponentManagerUrn();
            x.testedClass = testClass.getName();
            if (testGroup != null)
                x.testedMethods = "Only group \"" + testGroup + "\" + dependencies";
            else
                x.testedMethods = "All";

            Marshaller m = JAXBContext.newInstance(XmlTestOverview.class).createMarshaller();
            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
//            m.marshal(x, System.out);
            m.marshal(x, file);
        } catch (JAXBException e) {
            throw new RuntimeException("Error creating XML", e);
        }

        disableRemoveSecurityRisks = false;
    }

    public void toXmlOverview(Writer writer, boolean showCredentials) {
        disableRemoveSecurityRisks = showCredentials; //a bit of a hack now, would be nice to do this cleaner

        try {
            XmlTestOverview x = new XmlTestOverview(resultList, getVersionString(), getEnvironment());
            x.testUserUrn = user.getUserUrnString();
            x.testUserAuhUrn = user.getUserAuthorityServer().getDefaultComponentManagerUrn();
            x.testedAuthUrn = testedAuthority.getServerToConnect() == null ? "" : testedAuthority.getServerToConnect().getDefaultComponentManagerUrn();
            x.testedClass = testClass.getName();
            if (testGroup != null)
                x.testedMethods = "Only group \"" + testGroup + "\" + dependencies";
            else
                x.testedMethods = "All";

            Marshaller m = JAXBContext.newInstance(XmlTestOverview.class).createMarshaller();
            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
            m.marshal(x, writer);
        } catch (JAXBException e) {
            e.printStackTrace();
        }

        disableRemoveSecurityRisks = false;
    }

    @Override
    public void htmlContent(HtmlCanvas html) throws IOException {
        html.h1().content("Test Settings");
        html.div(class_("test-settings"));
        addKeyValue(html, "value-data", "Test User URN", user.getUserUrnString());
        addKeyValue(html, "value-data", "Test User Authority", user.getUserAuthorityServer().getDefaultComponentManagerUrn());
        html.br();
        addKeyValue(html, "value-data", "Tested Aggregate Manager (if applicable)", testedAuthority.getServerToConnect().getDefaultComponentManagerUrn());
        html.br();
        addKeyValue(html, "value-data", "Test Class", testClass.getName());
        if (testGroup != null)
            addKeyValue(html, "value-nondata", "Tested Methods", "Only group \"" + testGroup + "\" + dependencies");
        else
            addKeyValue(html, "value-nondata", "Tested Methods", "All");
        try {
            addKeyValue(html, "value-data", "Test Description", ApiTest.getDescription(testClass));
        } catch (Exception e) {
            LOG.error("Error while retrieving Test Description", e);
            addKeyValue(html, "value-data", "Test Description", "Error while retrieving: " + e.getMessage());
        }

        html.br();
        addKeyValue(html, "value-data", "Tester Version", getVersionString());
        addKeyValue(html, "value-data", "Tester Environment", getEnvironment());

        html._div();

        html.h1().content("Overview");

        html.div(class_("times"));
        html.span(class_("timeheader")).content("Total duration ");
        html.span(class_("value-nondata")).content((getDurationMs() / 1000.0) + "s");
        html.span(class_("timeheader")).content(" from ");
        html.span(class_("value-nondata")).content(getStartDate() + "");
        html.span(class_("timeheader")).content(" to ");
        html.span(class_("value-nondata")).content(getStopDate() + "");
        html._div();

//            html.p().content("Test ran at: "+test.);

        int index = 0;
        for (ApiTestMethodResult res : resultList)
            writeOverviewItem(html, index++, res.state, res.methodName);

        index = 0;
        html.h1().content("Details");
        for (ApiTestMethodResult res : resultList) {
            writeGroup(html, index++, res.state, res.methodName, res.description,
                    res.timeMs, new Date(res.startTimeMs), new Date(res.stopTimeMs),
                    res.exception, res.apiCallDetails, res.getLogLines());
        }
    }
}
