/*
 * Copyright 2000-2018 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 jetbrains.buildServer.xmlReportPlugin.parsers.mstest;

import java.util.List;
import jetbrains.buildServer.util.StringUtil;
import jetbrains.buildServer.xmlReportPlugin.parsers.BaseXmlXppAbstractParser;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * @author Eugene Petrenko
 *         Created: 24.10.2008 15:02:03
 */
class TestResultsTableParser extends BaseXmlXppAbstractParser {
  private final Callback myCallback;
  private final DurationParser myDurationParser = new DurationParser();

  public TestResultsTableParser(final Callback callback) {
    myCallback = callback;
  }

  @Override
  protected List<XmlHandler> getRootHandlers() {
    return new ORHandler(getRootHandler9(), getRootHandler8()) {
      @Override
      protected void finished(final boolean matched) {
        if (!matched) myCallback.error("Unexpected report format: root element must be one of \"Tests\" or \"TestRun\"\nPlease check Microsoft documentation for the supported schema" );
      }
    }.asList();
  }

  protected XmlHandler getRootHandler8() {
    return elementsPath(new Handler() {
      public XmlReturn processElement(@NotNull XmlElementInfo reader) {
        final TestResult result = new TestResult(TestResult.VS_Version.VS_8);

        return reader.visitChildren(
          elementsPath(new TextHandler() {
            public void setText(@NotNull final String text) {
              result.setTestId(text);
            }
          }, "id", "testId", "id"),
          elementsPath(new TextHandler() {
            public void setText(@NotNull final String text) {
              result.setDuration(text);
            }
          }, "duration"),
          elementsPath(new Handler() {
            public XmlReturn processElement(@NotNull XmlElementInfo reader) {
              return reader.visitChildren(
                elementsPath(new TextHandler() {
                  public void setText(@NotNull final String text) {
                    result.setError(text);
                  }
                }, "message"),
                elementsPath(new TextHandler() {
                  public void setText(@NotNull final String text) {
                    result.setStacktrace(text);
                  }
                }, "stackTrace")
              );
            }
          }, "errorInfo"),
          elementsPath(new TextHandler() {
            public void setText(@NotNull final String text) {
              result.addTrace(text);
            }
          }, "traceInfo", "trace"),
          elementsPath(new TextHandler() {
            public void setText(@NotNull final String text) {
              result.setOutcome(text);
            }
          }, "outcome", "value__"),
          elementsPath(new TextHandler() {
            public void setText(@NotNull final String text) {
              result.setStdOutput(text);
            }
          }, "stdout"),
          elementsPath(new TextHandler() {
            public void setText(@NotNull final String text) {
              result.setStdError(text);
            }
          }, "stderr")
        ).than(new XmlAction() {
          public void apply() {
            processTest(result);
          }
        });
      }
    }, "Tests", "UnitTestResult");
  }

  private void processRunError(final String text, final String exception) {
    myCallback.warning(text, exception);
  }

  private void processTest(final TestResult result) {
    final TestName testId = result.getTestName();
    if (testId == null) {
      myCallback.warning(testId, "Failed to read testId");
      return;
    }

    TestOutcome testOutcome = null;
    switch (result.getVersion()) {
      case VS_8:
        testOutcome = TestOutcome.parse8(result.getOutcome());
        break;
      case VS_9:
        testOutcome = TestOutcome.parse9(result.getOutcome());
        break;
    }

    myCallback.testFound(testId);

    final String stdOut = result.getStdOutput();
    if (stdOut != null) {
      myCallback.testOutput(testId, stdOut);
    }

    for (String trace : result.getTraces()) {
      if (!StringUtil.isEmptyOrSpaces(trace)) {
        myCallback.testOutput(testId, trace);
      }
    }

    final String stdErr = result.getStdError();
    if (stdErr != null) {
      myCallback.testError(testId, stdErr);
    }

    String error = result.getError();
    String stacktrace = result.getStacktrace();

    if (StringUtil.isEmptyOrSpaces(error) && StringUtil.isEmptyOrSpaces(stacktrace)) {
      error = testOutcome.getOutcomeName();
    }

    if (testOutcome.isIgnored()) {
      myCallback.testIgnored(testId, error, stacktrace);
    } else if (testOutcome.isFailed()) {
      myCallback.testException(testId, error, stacktrace);
    }

    final long duration = parseDuration(result, testId);
    myCallback.testFinished(testId, testOutcome, duration);
  }

  private long parseDuration(final TestResult result, final TestName testId) {
    final String sDuration = result.getDuration();
    final String startTime = result.getStartTime();
    final String endTime = result.getEndTime();

    if (!StringUtil.isEmptyOrSpaces(sDuration)) {
      final long duration = myDurationParser.parseTestDuration(sDuration);
      if (duration >= 0) return duration;
      myCallback.warning(testId, "Failed to parse duration from duration attribute '" + sDuration + "'. 0ms is chosen");
    }

    if (!StringUtil.isEmptyOrSpaces(startTime) && !StringUtil.isEmptyOrSpaces(endTime)) {
      final long duration = myDurationParser.parseTestDuration(startTime, endTime);
      if (duration >= 0) return duration;
      myCallback.warning(testId, "Failed to parse duration from startTime and endTime atributes '" + sDuration + "'. 0ms is chosen");
    }
    
    myCallback.warning(testId, "Failed to find test duration. 0ms is chosen");
    return 0;
  }

  protected XmlHandler getRootHandler9() {
    return elementsPath(
      new Handler() {
        public XmlReturn processElement(@NotNull final XmlElementInfo reader) {
          return reader.visitChildren(
              elementsPath(new Handler() {
                public XmlReturn processElement(@NotNull XmlElementInfo reader) {
                  final String outcome = reader.getAttribute("outcome");
                  final String[] textAndException = {null, null};

                  final TestOutcome testOutcome = TestOutcome.parse9(outcome);
                  if (testOutcome.isFailed() || testOutcome.isIgnored()) {
                    return reader.visitChildren(
                      elementsPath(new TextHandler() {
                        public void setText(@NotNull final String text) {
                          textAndException[0] = text;
                        }
                      }, "Text"),
                      elementsPath(new TextHandler() {
                        public void setText(@NotNull final String text) {
                          textAndException[1] = text;
                        }
                      }, "Exception")
                    ).than(new XmlAction() {
                      public void apply() {
                        if (textAndException[0] != null || textAndException[1] != null) {
                          processRunError(textAndException[0], textAndException[1]);
                        }
                      }
                    });
                  }
                  return reader.noDeep();
                }
              }, "ResultSummary", "RunInfos", "RunInfo"),
              elementsPath(new Handler() {
                public XmlReturn processElement(@NotNull final XmlElementInfo reader) {
                  return reader.visitChildren(getUnknown2008RecursiveResult(".*Result"));
                }
              }, "Results")
            );
        }
      }, "TestRun");
  }
  private XmlHandler[] getUnknown2008RecursiveResult(final String pattern) {
    return new XmlHandler[]{
      getUnknown2008Result(pattern),
      elementsPath(getUnknown2008Result(pattern), "TestResultAggregation"),
    };
  }


  private XmlHandler getUnknown2008Result(final String pattern) {
    return elementsPatternPath(new Handler() {
      public XmlReturn processElement(@NotNull final XmlElementInfo reader) {
        final TestResult result = new TestResult(TestResult.VS_Version.VS_9);

        result.setTestId(reader.getAttribute("testId"));
        result.setOutcome(reader.getAttribute("outcome"));
        result.setDuration(reader.getAttribute("duration"));
        result.setDataRowInfo(reader.getAttribute("dataRowInfo"));
        result.setStartTime(reader.getAttribute("startTime"));
        result.setEndTime(reader.getAttribute("endTime"));

        return reader.visitChildren(
          elementsPath(new Handler() {
            public XmlReturn processElement(@NotNull final XmlElementInfo reader) {
              result.setHasInnerResults(true);
              return reader.visitChildren(getUnknown2008RecursiveResult(pattern));
            }
          }, "InnerResults"),
          elementsPath(new Handler() {
            public XmlReturn processElement(@NotNull XmlElementInfo reader) {
              return reader.visitChildren(
                elementsPath(new TextHandler() {
                  public void setText(@NotNull final String text) {
                    result.setStdOutput(text);
                  }
                }, "StdOut"),
                elementsPath(new TextHandler() {
                  public void setText(@NotNull final String text) {
                    result.setStdError(text);
                  }
                }, "StdErr"),
                elementsPath(new Handler() {
                  public XmlReturn processElement(@NotNull XmlElementInfo reader) {
                    return reader.visitChildren(
                      elementsPath(new TextHandler() {
                        public void setText(@NotNull final String text) {
                          result.setError(text);
                        }
                      }, "Message"),
                      elementsPath(new TextHandler() {
                        public void setText(@NotNull final String text) {
                          result.setStacktrace(text);
                        }
                      }, "StackTrace")
                    );
                  }
                }, "ErrorInfo"));
            }
          }, "Output")).than(new XmlAction() {
          public void apply() {
            if (!result.hasInnerResults()) {
              processTest(result);
            }
          }
        });
      }
    }, pattern);
  }
  public static interface Callback {
    void testFound(@NotNull TestName testId);

    void testOutput(@NotNull TestName testId, @NotNull String text);

    void testError(@NotNull TestName testId, @NotNull String text);

    void testException(@NotNull TestName testId, @Nullable String message, @Nullable String error);

    void testFinished(@NotNull TestName testId, @NotNull TestOutcome outcome, long duration);

    void testIgnored(@NotNull TestName testId, @Nullable String message, @Nullable String error);

    void warning(@Nullable TestName testId, @NotNull String message);

    void warning(@Nullable String message, @Nullable String exception);

    void error(@NotNull String message);
  }
}
