package org.infinispan.commons.test;


import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

/**
 * A JUnit XML report generator for Polarion based on the JUnitXMLReporter
 *
 * <p>Extracted from {@link PolarionJUnitXMLReporter}</p>
 *
 * @author <a href='mailto:afield[at]redhat[dot]com'>Alan Field</a>
 * @author Dan Berindei
 * @since 10.0
 */
public class PolarionJUnitXMLWriter implements AutoCloseable {

   private final FileWriter fileWriter;

   public enum Status {SUCCESS, FAILURE, ERROR, SKIPPED}

   private static final String TESTSUITE = "testsuite";
   private static final String ATTR_TESTS = "tests";
   private static final String ATTR_TIME = "time";
   private static final String ATTR_NAME = "name";
   private static final String ATTR_SKIPPED = "skipped";
   private static final String ATTR_ERRORS = "errors";
   private static final String ATTR_FAILURES = "failures";

   private static final String TESTCASE = "testcase";
   private static final String FAILURE = "failure";
   private static final String ERROR = "error";
   private static final String SKIPPED = "skipped";
   private static final String ATTR_CLASSNAME = "classname";
   private static final String ATTR_MESSAGE = "message";
   private static final String ATTR_TYPE = "type";
   private static final String ATTR_VALUE = "value";

   private static final String PROPERTIES = "properties";
   private static final String PROPERTY = "property";

   private static final char UNICODE_REPLACEMENT = 0xFFFD;

   private static XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory();

   private XMLStreamWriter xmlWriter;

   public PolarionJUnitXMLWriter(File outputFile) throws IOException {
      File outputDirectory = outputFile.getParentFile();
      if (!outputDirectory.exists()) {
         if (!outputDirectory.mkdirs()) {
            throw new IllegalStateException("Unable to create report directory " + outputDirectory);
         }
      }
      fileWriter = new FileWriter(outputFile);
   }

   public void start(String moduleName, long testCount, long skippedCount, long failedCount, long elapsedTime,
                     boolean includeProperties) throws XMLStreamException {
      xmlWriter = new PrettyXMLStreamWriter(xmlOutputFactory.createXMLStreamWriter(fileWriter));

      xmlWriter.writeStartDocument();
      xmlWriter.writeCharacters("\n");
      xmlWriter.writeComment("Generated by " + getClass().getName());

      xmlWriter.writeStartElement(TESTSUITE);
      xmlWriter.writeAttribute(ATTR_TESTS, "" + testCount);
      xmlWriter.writeAttribute(ATTR_TIME, "" + elapsedTime / 1000.0);
      xmlWriter.writeAttribute(ATTR_NAME, moduleName);
      xmlWriter.writeAttribute(ATTR_SKIPPED, "" + skippedCount);
      xmlWriter.writeAttribute(ATTR_ERRORS, "0");
      xmlWriter.writeAttribute(ATTR_FAILURES, "" + failedCount);

      if (includeProperties) {
         writeProperties();
      }

      xmlWriter.writeComment("Tests results");
   }

   @Override
   public void close() throws Exception {
      xmlWriter.writeEndElement();
      xmlWriter.writeEndDocument();
      xmlWriter.close();
      fileWriter.close();
   }

   public void writeTestCase(String testName, String className, long elapsedTimeMillis, Status status,
                             String stackTrace, String throwableClass, String throwableMessage)
      throws XMLStreamException {

      String elapsedTime = "" + (((double) elapsedTimeMillis) / 1000);
      if (Status.SUCCESS == status) {
         xmlWriter.writeEmptyElement(TESTCASE);
      } else {
         xmlWriter.writeStartElement(TESTCASE);
      }

      xmlWriter.writeAttribute(ATTR_NAME, testName);
      xmlWriter.writeAttribute(ATTR_CLASSNAME, className);
      xmlWriter.writeAttribute(ATTR_TIME, elapsedTime);

      if (Status.SUCCESS != status) {
         switch (status) {
            case FAILURE:
               writeCauseElement(FAILURE, throwableClass, throwableMessage, stackTrace);
               break;
            case ERROR:
               writeCauseElement(ERROR, throwableClass, throwableMessage, stackTrace);
               break;
            case SKIPPED:
               writeSkipElement();
         }
         xmlWriter.writeEndElement();
      }
   }

   private void writeCauseElement(String tag, String throwableClass, String message, String stackTrace)
      throws XMLStreamException {
      xmlWriter.writeStartElement(tag);
      xmlWriter.writeAttribute(ATTR_TYPE, throwableClass);
      if ((message != null) && (message.length() > 0)) {
         xmlWriter.writeAttribute(ATTR_MESSAGE, escapeInvalidChars(message));
      }
      xmlWriter.writeCData(stackTrace);
      xmlWriter.writeEndElement();
   }

   private void writeSkipElement() throws XMLStreamException {
      xmlWriter.writeEmptyElement(SKIPPED);
   }

   private String escapeInvalidChars(String chars) {
      // Restricted chars for xml are [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F]
      return chars.codePoints()
                  .map(c -> {
                     if (!Character.isDefined(c) || 0x1 <= c && c <= 0x8 || c == 0xB || c == 0xC ||
                         0xE <= c && c <= 0x1F || 0x7F <= c && c < 0x84 || 0x86 <= c && c <= 0x9F) {
                        return UNICODE_REPLACEMENT;
                     } else {
                        return c;
                     }
                  })
                  .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                  .toString();
   }

   private void writeProperties() throws XMLStreamException {
      xmlWriter.writeStartElement(PROPERTIES);

      // Add all system properties
      xmlWriter.writeComment("Java System properties");
      for (Object key : System.getProperties().keySet()) {
         xmlWriter.writeEmptyElement(PROPERTY);
         xmlWriter.writeAttribute(ATTR_NAME, key.toString());
         xmlWriter.writeAttribute(ATTR_VALUE, System.getProperty(key.toString()));
      }

      // Add all environment variables
      xmlWriter.writeComment("Environment variables");
      for (String key : System.getenv().keySet()) {
         xmlWriter.writeEmptyElement(PROPERTY);
         xmlWriter.writeAttribute(ATTR_NAME, key);
         xmlWriter.writeAttribute(ATTR_VALUE, System.getenv(key));
      }

      xmlWriter.writeEndElement();
   }
}
