/*
 * Copyright 2000-2010 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.execution.testframework.export;

import com.intellij.execution.DefaultExecutionTarget;
import com.intellij.execution.ExecutionBundle;
import com.intellij.execution.ExecutionTarget;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.filters.HyperlinkInfo;
import com.intellij.execution.impl.ConsoleBuffer;
import com.intellij.execution.testframework.*;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import org.jdom.Attribute;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import java.text.SimpleDateFormat;
import java.util.*;

public class TestResultsXmlFormatter {

  private static final String ELEM_RUN = "testrun";
  public static final String ELEM_TEST = "test";
  public static final String ELEM_SUITE = "suite";
  public static final String ATTR_NAME = "name";
  public static final String ATTR_DURATION = "duration";
  public static final String ATTR_LOCATION = "locationUrl";
  public static final String ELEM_COUNT = "count";
  public static final String ATTR_VALUE = "value";
  public static final String ELEM_OUTPUT = "output";
  public static final String ATTR_OUTPUT_TYPE = "type";
  public static final String ATTR_STATUS = "status";
  public static final String TOTAL_STATUS = "total";
  private static final String ATTR_FOORTER_TEXT = "footerText";
  public static final String ATTR_CONFIG = "isConfig";
  public static final String STATUS_PASSED = "passed";
  public static final String STATUS_FAILED = "failed";
  public static final String STATUS_ERROR = "error";
  public static final String STATUS_IGNORED = "ignored";
  public static final String STATUS_SKIPPED = "skipped";

  public static final String ROOT_ELEM = "root";
  

  private final RunConfiguration myRuntimeConfiguration;
  private final ContentHandler myResultHandler;
  private final AbstractTestProxy myTestRoot;
  private final boolean myHidePassedConfig;
  private final ExecutionTarget myExecutionTarget;

  public static void execute(AbstractTestProxy root, RunConfiguration runtimeConfiguration, TestConsoleProperties properties, ContentHandler resultHandler)
    throws SAXException {
    new TestResultsXmlFormatter(root, runtimeConfiguration, properties, resultHandler).execute();
  }

  private TestResultsXmlFormatter(AbstractTestProxy root,
                                  RunConfiguration runtimeConfiguration,
                                  TestConsoleProperties properties,
                                  ContentHandler resultHandler) {
    myRuntimeConfiguration = runtimeConfiguration;
    myTestRoot = root;
    myResultHandler = resultHandler;
    myHidePassedConfig = TestConsoleProperties.HIDE_SUCCESSFUL_CONFIG.value(properties);
    myExecutionTarget = properties.getExecutionTarget();
  }

  private void execute() throws SAXException {
    myResultHandler.startDocument();

    TreeMap<String, Integer> counts = new TreeMap<>((o1, o2) -> {
      if (TOTAL_STATUS.equals(o1) && !TOTAL_STATUS.equals(o2)) return -1;
      if (TOTAL_STATUS.equals(o2) && !TOTAL_STATUS.equals(o1)) return 1;
      return o1.compareTo(o2);
    });
    for (AbstractTestProxy node : myTestRoot.getAllTests()) {
      if (!node.isLeaf()) continue;
      String status = getStatusString(node);
      increment(counts, status);
      increment(counts, TOTAL_STATUS);
    }

    Map<String, String> runAttrs = new HashMap<>();
    runAttrs.put(ATTR_NAME, myRuntimeConfiguration.getName());
    String footerText = ExecutionBundle.message("export.test.results.footer", ApplicationNamesInfo.getInstance().getFullProductName(),
                                                new SimpleDateFormat().format(new Date()));
    runAttrs.put(ATTR_FOORTER_TEXT, footerText);
    Long duration = myTestRoot.getDuration();
    if (duration != null) {
      runAttrs.put(ATTR_DURATION, String.valueOf(duration));
    }
    startElement(ELEM_RUN, runAttrs);

    for (Map.Entry<String, Integer> entry : counts.entrySet()) {
      Map<String, String> a = new HashMap<>();
      a.put(ATTR_NAME, entry.getKey());
      a.put(ATTR_VALUE, String.valueOf(entry.getValue()));
      startElement(ELEM_COUNT, a);
      endElement(ELEM_COUNT);
    }

    final Element config = new Element("config");
    try {
      myRuntimeConfiguration.writeExternal(config);
      config.setAttribute("configId", myRuntimeConfiguration.getType().getId());
      config.setAttribute("name", myRuntimeConfiguration.getName());
      if (!DefaultExecutionTarget.INSTANCE.equals(myExecutionTarget)) {
        config.setAttribute("target", myExecutionTarget.getId());
      }
    }
    catch (WriteExternalException ignore) {}
    processJDomElement(config);

    if (myTestRoot instanceof TestProxyRoot) {
      final String presentation = ((TestProxyRoot)myTestRoot).getPresentation();
      if (presentation != null) {
        final LinkedHashMap<String, String> rootAttrs = new LinkedHashMap<>();
        rootAttrs.put("name", presentation);
        final String comment = ((TestProxyRoot)myTestRoot).getComment();
        if (comment != null) {
          rootAttrs.put("comment", comment);
        }
        final String rootLocation = ((TestProxyRoot)myTestRoot).getRootLocation();
        if (rootLocation != null) {
          rootAttrs.put("location", rootLocation);
        }
        startElement(ROOT_ELEM, rootAttrs);
        writeOutput(myTestRoot);
        endElement(ROOT_ELEM);
      }
    }
    
    if (myTestRoot.shouldSkipRootNodeForExport()) {
      for (AbstractTestProxy node : myTestRoot.getChildren()) {
        processNode(node);
      }
    }
    else {
      processNode(myTestRoot);
    }
    endElement(ELEM_RUN);
    myResultHandler.endDocument();
  }

  private void processJDomElement(Element config) throws SAXException {
    final String name = config.getName();
    final LinkedHashMap<String, String> attributes = new LinkedHashMap<>();
    for (Attribute attribute : config.getAttributes()) {
      attributes.put(attribute.getName(), attribute.getValue());
    }
    startElement(name, attributes);
    for (Element child : config.getChildren()) {
      processJDomElement(child);
    }
    endElement(name);
  }

  private static void increment(Map<String, Integer> counts, String status) {
    Integer count = counts.get(status);
    counts.put(status, count != null ? count + 1 : 1);
  }

  private void processNode(AbstractTestProxy node) throws SAXException {
    ProgressManager.checkCanceled();
    Map<String, String> attrs = new HashMap<>();
    attrs.put(ATTR_NAME, node.getName());
    attrs.put(ATTR_STATUS, getStatusString(node));
    Long duration = node.getDuration();
    if (duration != null) {
      attrs.put(ATTR_DURATION, String.valueOf(duration));
    }
    String locationUrl = node.getLocationUrl();
    if (locationUrl != null) {
      attrs.put(ATTR_LOCATION, locationUrl);
    }
    if (node.isConfig()) {
      attrs.put(ATTR_CONFIG, "true");
    }
    boolean started = false;
    String elemName = node.isLeaf() ? ELEM_TEST : ELEM_SUITE;
    if (node.isLeaf()) {
      started = true;
      startElement(elemName, attrs);
      writeOutput(node);
    }
    else {
      for (AbstractTestProxy child : node.getChildren()) {
        if (myHidePassedConfig && child.isConfig() && child.isPassed()) {
          //ignore configurations during export
          continue;
        }
        if (!started) {
          started = true;
          startElement(elemName, attrs);
        }
        processNode(child);
      }
    }
    if (started) {
      endElement(elemName);
    }
  }

  private void writeOutput(AbstractTestProxy node) throws SAXException {
    final StringBuilder buffer = new StringBuilder();
    final Ref<ConsoleViewContentType> lastType = new Ref<>();
    final Ref<SAXException> error = new Ref<>();

    final int bufferSize = ConsoleBuffer.useCycleBuffer() ? ConsoleBuffer.getCycleBufferSize() : -1;
    final Printer printer = new Printer() {
      @Override
      public void print(String text, ConsoleViewContentType contentType) {
        ProgressManager.checkCanceled();
        if (contentType != lastType.get()) {
          if (buffer.length() > 0) {
            try {
              writeOutput(lastType.get(), buffer);
            }
            catch (SAXException e) {
              error.set(e);
            }
          }
          lastType.set(contentType);
        }
        if (bufferSize < 0 || buffer.length() < bufferSize) {
          buffer.append(text);
        }
      }

      @Override
      public void onNewAvailable(@NotNull Printable printable) {
      }

      @Override
      public void printHyperlink(String text, HyperlinkInfo info) {
      }

      @Override
      public void mark() {
      }
    };
    node.printOwnPrintablesOn(printer);
    if (!error.isNull()) {
      throw error.get();
    }
    if (buffer.length() > 0) {
      writeOutput(lastType.get(), buffer);
    }
  }

  private void writeOutput(ConsoleViewContentType type, StringBuilder text) throws SAXException {
    StringBuilder output = new StringBuilder();
    StringTokenizer t = new StringTokenizer(text.toString(), "\n");
    while (t.hasMoreTokens()) {
      output.append(StringUtil.escapeXml(t.nextToken())).append("\n");
    }

    Map<String, String> a = new HashMap<>();
    a.put(ATTR_OUTPUT_TYPE, getTypeString(type));
    startElement(ELEM_OUTPUT, a);
    writeText(output.toString());
    text.delete(0, text.length());
    endElement(ELEM_OUTPUT);
  }

  private static String getTypeString(ConsoleViewContentType type) {
    return type == ConsoleViewContentType.ERROR_OUTPUT ? "stderr" : "stdout";
  }

  private static String getStatusString(AbstractTestProxy node) {
    int magnitude = node.getMagnitude();
    // TODO enumeration!
    switch (magnitude) {
      case 0:
        return STATUS_SKIPPED;
      case 2:
      case 4:
        return STATUS_SKIPPED;
      case 5:
        return STATUS_IGNORED;
      case 1:
        return STATUS_PASSED;
      case 6:
        return STATUS_FAILED;
      case 8:
        return STATUS_ERROR;
      default:
        return node.isPassed() ? STATUS_PASSED : STATUS_FAILED;
    }
  }

  private void startElement(String name, Map<String, String> attributes) throws SAXException {
    AttributesImpl attrs = new AttributesImpl();
    for (Map.Entry<String, String> entry : attributes.entrySet()) {
      attrs.addAttribute("", entry.getKey(), entry.getKey(), "CDATA", entry.getValue());
    }
    myResultHandler.startElement("", name, name, attrs);
  }

  private void endElement(String name) throws SAXException {
    myResultHandler.endElement("", name, name);
  }

  private void writeText(String text) throws SAXException {
    final char[] chars = text.toCharArray();
    myResultHandler.characters(chars, 0, chars.length);
  }
}
