/*! @file

    @id $Id$
*/
//       1         2         3         4         5         6         7         8
// 45678901234567890123456789012345678901234567890123456789012345678901234567890
#ifndef COMMANDS_HXX
#define COMMANDS_HXX

#include <exceptions.hxx>
#include <webpage.hxx>
#include <QNetworkReply>
#include <QAuthenticator>
#include <QCoreApplication>
#include <QStringList>
#include <QWebFrame>
#include <QWebView>
#include <QWebElement>
#include <QPainter>
#include <QImage>
#include <QSslKey>
#include <QTimer>
#include <QProcess>
#include <QMouseEvent>
#include <QRegularExpression>
#include <QNetworkCookieJar>
#include <QNetworkCookie>
#include <QSet>
#include <vector>
#include <queue>
#include <map>
#include <memory>
#include <sstream>
#include <cassert>
#include <xml-cxx/xml.hxx>
#include <mrw/stdext.hxx>

#ifdef HAVE_CXXABI_H
#include <cxxabi.h>
inline QString demangle(const char* mangled) {
  int status;
  std::unique_ptr<char[], void (*)(void*)> result(
    abi::__cxa_demangle(mangled, 0, 0, &status), free);
  return QString(result.get() ? result.get() : mangled);
}
#else
inline QString demangle(const char* mangled) {
  return QString(mangled);
}
#endif

namespace std {
  template<typename T> class optional {
    private:
      T* _opt;
      bool _set;
    public:
      optional():
          _opt(0), _set(false) {
      }
      optional(const T& other):
          _opt(new T(other)), _set(true) {
      }
      ~optional() {
        if (_set) delete _opt;
      }
      optional& operator=(const T& other) {
        if (_set) delete _opt;
        _set = true;
        _opt = new T(other);
        return *this;
      }
      T* operator->() {
        return _opt;
      }
      T& operator*() {
        return *_opt;
      }
      const T* operator->() const {
        return _opt;
      }
      const T& operator*() const {
        return *_opt;
      }
      operator bool() const {
        return _set;
      }
  };
}

class Script;
class Command;
class Function;

class Logger {
  public:
    Logger(Command* command, Script* script, bool showLines = true);
    void operator[](QString txt);
    void operator()(QString txt);
    ~Logger();
  private:
    Command* _command;
    Script* _script;
};

class Command: public QObject {
    Q_OBJECT
  public:
    Command(): _log(true), _line(-1), _indent(0) {}
    virtual ~Command() {}
    virtual QString tag() const = 0;
    virtual QString description() const = 0;
    virtual QString command() const = 0;
    virtual std::shared_ptr<Command> parse(Script*, QString,
                                           QStringList&, QString, int, int) = 0;
    virtual bool execute(Script*, QWebFrame*) = 0;
    static void error(Logger& log, const Exception& e) {
      log(QString("  FAILED[")+demangle(typeid(e).name())+"]: "+e.what());
      throw e;
    }
    virtual int steps(Script*) {
      return 1;
    }
    void line(int linenr) {
      _line = linenr;
    }
    int line() const {
      return _line;
    }
    void file(QString filename) {
      _file = filename;
    }
    QString file() const {
      return _file;
    }
    void indent(int i) {
      _indent = i;
    }
    int indent() const {
      return _indent;
    }
    bool log() {
      return _log;
    }
    void log(bool l) {
      _log = l;
    }
    QString result() {
      return _result;
    }
    virtual bool isTestcase() {
      return true;
    }
    static void realMouseClick(Logger& log, QWebFrame* frame,
                               Script* script, QString selector);
    static void realMouseClick(const QWebElement& element) {
      QWidget* web(element.webFrame()->page()->view());
      QRect elGeom=element.geometry();
      QPoint elPoint=elGeom.center();
      int elX=elPoint.x();
      int elY=elPoint.y();
      int webWidth=web->width();
      int webHeight=web->height();
      int pixelsToScrolRight=0;
      int pixelsToScrolDown=0;
      if (elX>webWidth)
        pixelsToScrolRight = //the +10 scrolls a bit further
          elX-webWidth+elGeom.width()/2+10;
      if (elY>webHeight)
        pixelsToScrolDown = //the +10 scrolls a bit further
          elY-webHeight+elGeom.height()/2+10;
      element.webFrame()->setScrollBarValue(Qt::Horizontal, pixelsToScrolRight);
      element.webFrame()->setScrollBarValue(Qt::Vertical, pixelsToScrolDown);
      QPoint pointToClick(elX-pixelsToScrolRight, elY-pixelsToScrolDown);
      QMouseEvent pressEvent(QMouseEvent::MouseButtonPress,
                             pointToClick, Qt::LeftButton, Qt::LeftButton,
                             Qt::NoModifier);
      QCoreApplication::sendEvent(web, &pressEvent);
      QMouseEvent releaseEvent(QMouseEvent::MouseButtonRelease,
                               pointToClick, Qt::LeftButton, Qt::LeftButton,
                               Qt::NoModifier);
      QCoreApplication::sendEvent(web, &releaseEvent);
      QCoreApplication::processEvents();
    }
    static void sleep(int s) {
      QTime dieTime= QTime::currentTime().addSecs(s);
      while (QTime::currentTime()<dieTime)
        QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
    }
  protected:
    std::shared_ptr<Script> subParser(Script* parent, const QStringList& in,
                                      const QString& file, int line, int indent);
    bool runScript(Logger& log, Command* parentCommand,
                   std::shared_ptr<Script> script,
                   Script* parent, QWebFrame* frame,
                   QStringList vars = QStringList(),
                   QStringList args = QStringList());
    QStringList subCommandBlock(QStringList& in) {
      QStringList commands;
      std::string::size_type pos(std::string::npos);
      while (in.size() && in[0].size() && in[0][0]==' '
             && (pos==std::string::npos ||
                 pos<=in[0].toStdString().find_first_not_of(' '))) {
        if (pos==std::string::npos) pos=in[0].toStdString().find_first_not_of(' ');
        commands += in.takeFirst().mid(pos);
      }
      return commands;
    }
    QStringList quotedStrings(QString value,
                              QString delimiter = " ",
                              bool keepDelimiters = false) const {
      QStringList res;
      QString quot("'\"");
      while (value=value.trimmed(), value.size()) {
        QRegularExpression re;
        int start(0);
        if (quot.contains(value[0])) {
          re = QRegularExpression(value[0]+" *(("+delimiter+" *)|$)");
          start = 1;
        } else {
          re = QRegularExpression("( *"+delimiter+" *)|$");
        }
        int pos(value.indexOf(re, start));
        if (pos<start) throw BadArgument("quote missmatch in "+value);
        QRegularExpressionMatch m(re.match(value, start));
        res += value.mid(start, m.capturedStart()-start);
        value.remove(0, m.capturedEnd());
        if (keepDelimiters && m.capturedLength()) res+=m.captured().mid(start).trimmed();
      }
      return res;
    }
    QStringList commaSeparatedList(QString value) const {
      return quotedStrings(value, ",");
    }
    static QWebElement find(QWebFrame* frame, QString selector,
                            int repeat = 2, int sleepsec = 1) {
      QWebElement element;
      element = find1(frame, selector, repeat, sleepsec);
      if (!element.isNull()) return element;
      element = find1(frame->page()->currentFrame(), selector,
                      repeat, sleepsec);
      if (!element.isNull()) return element;
      element = find1(frame->page()->mainFrame(), selector,
                      repeat, sleepsec);
      if (!element.isNull()) return element;
      return element;
    }    
    static QWebElement find1(QWebFrame* frame, QString selector,
                             int repeat = 2, int sleepsec = 1) {
      QWebElement element;
      if (repeat<1) repeat=1;
      for (int i=0; i<repeat; ++i) {
        element = frame->findFirstElement(selector);
        if (!element.isNull()) return element;
        for (QWebFrame* childFrame: frame->childFrames()) {
          element = find1(childFrame, selector, 1, 0);
          if (!element.isNull()) return element;
        }
        if (i+1<repeat && sleepsec) sleep(sleepsec);
      }
      return element;
    }
    void log(Script*);
    bool _log;
  protected:
    QString _result;
  private:
    int _line;
    QString _file;
    int _indent;
};

class Empty: public Command {
  public:
    QString tag() const {
      return "";
    }
    QString description() const {
      return
        ""
        "\n\n"
        "Empty lines are allowed";
    }
    QString command() const {
      return tag();
    }
    std::shared_ptr<Command> parse(Script*, QString,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Empty> cmd(new Empty());
      return cmd;
    }
    bool execute(Script*, QWebFrame*) {
      return true;
    }
    virtual bool isTestcase() {
      return false;
    }
};

class Comment: public Command {
  public:
    Comment(QString comment): _comment(comment) {}
    QString tag() const {
      return "#";
    }
    QString description() const {
      return
        "# comment"
        "\n\n"
        "Comments are lines that start with #";
    }
    QString command() const {
      return _comment;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Comment> cmd(new Comment(args));
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      this->log(false);
      Logger log(this, script);
      this->log(true);
      log(_comment);
      this->log(false);
      return true;
    }
    virtual bool isTestcase() {
      return false;
    }
  private:
    QString _comment;
};

class Screenshot: public Command {
  public:
    static QString sourceHtml(Logger& log, int line, QString target, QString base,
                              QString name, QWebFrame* frame) {
      if (!QDir(target).exists() && !QDir().mkpath(target))
        error(log, DirectoryCannotBeCreated(target));
      QCoreApplication::processEvents();
      QString filename(target+QDir::separator()+
                       QString("%4-%1-%2-%3.html")
                       .arg(base)
                       .arg(line, 4, 10, QChar('0'))
                       .arg(name)
                       .arg(QDateTime::currentDateTime()
                            .toString("yyyyMMddHHmmss")));
      QFile file(filename);
      if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
        error(log, CannotWriteSouceHTML(filename));
      QTextStream out(&file);
      out<<frame->toHtml();
      if (out.status()!=QTextStream::Ok) error(log, CannotWriteSouceHTML(filename));
      return QDir(filename).absolutePath();
    }
    static QString screenshot(Logger& log, int line, QString target, QString base,
                              QString name, QWebFrame* frame) {
      bool wasShown(frame->page()->view()->isVisible());
      frame->page()->view()->show();
      QCoreApplication::processEvents();
      QImage image(frame->page()->view()->size(), QImage::Format_RGB32);
      QPainter painter(&image);
      frame->render(&painter);
      painter.end();
      if (!wasShown) frame->page()->view()->hide();
      if (!QDir(target).exists() && !QDir().mkpath(target))
        error(log, DirectoryCannotBeCreated(target));
      QCoreApplication::processEvents();
      QString filename(target+QDir::separator()+
                       QString("%4-%1-%2-%3.png")
                       .arg(base)
                       .arg(line, 4, 10, QChar('0'))
                       .arg(name)
                       .arg(QDateTime::currentDateTime()
                            .toString("yyyyMMddHHmmss")));
      if (!image.save(filename)) error(log, CannotWriteScreenshot(filename));
      return QDir(filename).absolutePath();
    }
    QString tag() const {
      return "screenshot";
    }
    QString description() const {
      return
        tag()+" <filename-base>"
        "\n\n"
        "Create a PNG screenshot of the actual web page and store it in the "
        "file <filename-base>.png. If not already opened, a browser window "
        "will pop up to take the screenshot.";
    }
    QString command() const {
      return tag()+" "+_filename;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Screenshot> cmd(new Screenshot());
      cmd->_filename = args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame);
  private:
    QString _filename;
};

class RunDownload: public QObject {
    Q_OBJECT
  public:
    RunDownload(QNetworkReply* reply, QString filename):
        _reply(reply), _file(filename) {
      assert(connect(_reply, SIGNAL(finished()), SLOT(finished())));
      assert(connect(_reply, SIGNAL(downloadProgress(qint64, qint64)),
                     SLOT(downloadProgress(qint64, qint64))));
      _file.open(QIODevice::WriteOnly);
    }
    ~RunDownload() {
      assert(disconnect(_reply, SIGNAL(finished()), this, SLOT(finished())));
      assert(disconnect(_reply, SIGNAL(downloadProgress(qint64, qint64)),
                        this, SLOT(downloadProgress(qint64, qint64))));
      delete _reply;
    }
  Q_SIGNALS:
    void completed(bool netsuccess, bool filesuccess);
  private Q_SLOTS:
    void finished() {
      _file.write(_reply->readAll());
      _file.close();
      completed(_reply->error()==QNetworkReply::NoError,
                _file.error()==QFileDevice::NoError);
      delete this;
    }
    void downloadProgress(qint64 bytes, qint64) {
      _file.write(_reply->read(bytes));
    }
  private:
    QNetworkReply* _reply;
    QFile _file;
};

/** @mainpage

    The WebTester web application testing framework consists of three parts:

      -# a gui to create testcases, see @ref webtester
      -# a command line tool to run testcases, see @ref webrunner
      -# test scripts,  see @ref testscript. */

class Script: public QObject {
    Q_OBJECT
  Q_SIGNALS:
    void logging(QString);
    void progress(QString, int, int, int);
  public:
    typedef std::pair<QString, QStringList> Signal;
    enum ClickType {
      REAL_MOUSE_CLICK,
      JAVASCRIPT_CLICK
    };
    enum Formatting {
      PLAIN,
      HTML
    };
    /// QString that sorts first by string length, then by content
    class LenString: public QString {
      public:
        LenString() {}
        LenString(const QString& o): QString(o) {}
        virtual bool operator<(const LenString& o) const {
          return
            size()<o.size() ? true : o.size()<size() ? false
            : QString::operator<(o.toLatin1());
        }
    };
  public:
    static QString xmlattr(QString attr, bool br = false) {
      if (br) return attr.toHtmlEscaped().replace("\n", "<br/>");
      return attr.toHtmlEscaped();
    }
    static QString xmlstr(const std::string& attr) {
      return xmlstr(QString::fromStdString(attr));
    }
    static QString xmlstr(QString attr) {
      return attr
        .replace("&gt;", ">").replace("&lt;", "<")
        .replace("<br/>", "\n").replace("&quot;", "\"")
        .replace("&nbsp;", " ").replace("&amp;", "&");
    }
  public:
    Script():
        _step(0), _clicktype(JAVASCRIPT_CLICK),
        _screenshots(true), _defaultTimeout(20) {
      initPrototypes();
    }
    Script(const Script& o):
        QObject(),
        _prototypes(o._prototypes),
        _step(0),
        _script(o._script),
        _screenshots(true) {
      set(o);
    }
    QString syntax() const {
      return
        "Script syntax is a text file that consists of list of commands. Each "
        "command starts at the begin of a new line. Empty lines are allowed. "
        "Lines that start with \"#\" are treated as comments."
        "\n\n"
        "Subcommands are indented. The first indented line defines the level of "
        "indentation. All following lines must be indented by the same level."
        "\n\n"
        "Note: When a selector is required as parameter, then the selector "
        "is a CSS selector."
        "\n\n"
        "Thanks to the filter script doxygen-webtester.sed, you cab use the "
        "comments for producing doxygen documenation. Just start comments with "
        "\"##\" to import them to doxygen. This script is automatically configured, "
        "when you use the autotools bootstrap from:\n"
        "https://dev.marc.waeckerlin.org/redmine/projects/bootstrap-build-environment";
    }
    /// set workdir
    void path(QString path) {
      _path = (path.size()?path:".")+QDir::separator();
    }
    /// get workdir
    QString path() {
      return _path;
    }
    QString commands(Formatting f = PLAIN) const {
      QString cmds;
      for (auto it(_prototypes.begin()); it!=_prototypes.end(); ++it)
        switch (f) {
          case PLAIN: {
            cmds+="\n\n\nCOMMAND: "+it->first+"\n\n"+it->second->description();
          } break;
          case HTML: {
            cmds+="<h1>"+it->first+"</h1><p>"
              +it->second->description()
              .replace("&", "&amp;")
              .replace("<", "&lt;")
              .replace(">", "&gt;")
              .replace(QRegularExpression("&lt;([^ ]+)&gt;"),
                       "<i>\\1</i>")
              .replace(QRegularExpression("(\n[^ ][^\n]*)(\n +-)"),
                       "\\1<ul>\\2")
              .replace(QRegularExpression("(\n +-[^\n]*\n)([^ ])"),
                       "\\1</ul>\\2")
              .replace(QRegularExpression("\n +- ([^\n]*)(</ul>)?"),
                       "<li>\\1</li>\\2")
              .replace("</li>\n", "</li>")
              .replace("\n  ", "\n&nbsp;&nbsp;")
              .replace("\n\n", "</p><p>")
              .replace("\n", "<br/>")
              +"</p>";
          } break;
      }
      return cmds.trimmed();
    }
    void reset() {
      _script.clear();
      while (!_signals.empty()) _signals.pop();
      _timer.stop();
      _ignores.clear();
      _cout.clear();
      _cerr.clear();
      _ignoreSignalsUntil.clear();
    }
    void cleanup() {
      reset();
      _ignore.clear();
      _variables.clear();
      _rvariables.clear();
      _timeout = _defaultTimeout;
      _step = 0;
      _clicktype = JAVASCRIPT_CLICK;
    }
    std::shared_ptr<Command> parseLine(QStringList& in,
                                       QString filename, int linenr,
                                       int indent) try {
      std::shared_ptr<Command> command;
      if (!in.size()) throw MissingLine();
      QString line(in.takeFirst().trimmed());
      QString cmd(line), args;
      int space(line.indexOf(' '));
      if (space>0) {
        cmd = line.left(space);
        args = line.right(line.size()-space-1);
      }
      Prototypes::const_iterator it(_prototypes.find(cmd));
      if (it!=_prototypes.end()) {
        command = it->second->parse(this, args, in, filename, linenr, indent);
      } else {
        command = unknown(line);
      }
      command->file(filename);
      command->line(linenr);
      command->indent(indent);
      return command;
    } catch (Exception& e) {
      e.prependFileLine(filename, linenr);
      throw;
    }
    void parse(QStringList in, QString filename, int line = 1, int indent = 0) {
      _filename = filename;
      for (int linenr(0), oldsize(0);
           oldsize=in.size(), in.size();
           linenr+=oldsize-in.size())
        _script.push_back(parseLine(in, filename, line+linenr, indent));
    }
    QStringList print() {
      QStringList result;
      for (auto cmd(_script.begin()); cmd!=_script.end(); ++cmd) {
        result += (*cmd)->command();
      }
      return result;
    }
    bool run(QWebFrame* frame) {
      return run(frame, _testsuites, targetdir(), _screenshots, _maxretries);
    }
    bool run(QWebFrame* frame, std::shared_ptr<xml::Node> testsuites,
             QString td = QString(), bool screenshots = true,
             int maxretries = 0) {
      bool res(true);
      _step = 0;
      _testsuites = testsuites;
      _timeout = _defaultTimeout; // defaults to 20s
      _ignoreSignalsUntil.clear();
      addSignals(frame);
      _screenshots = screenshots;
      _maxretries = maxretries;
      _timer.setSingleShot(true);
      targetdir(!td.isEmpty()
                ? td
                : _testsuites->children()
                ? xmlstr(_testsuites->last().attr("name"))
                : "attachments");
      if (!_testsuites->children()) {
        xml::Node testsuite("testsuite");
        testsuite.attr("name") = "Unnamed Test Suite";
        (*_testsuites)<<testsuite;
      }
      int retries(0), back(0);
      for (auto cmd(_script.begin()); cmd!=_script.end();
           _step+=(*cmd)->steps(this), ++cmd) {
        progress((*cmd)->file(), (*cmd)->line(), _step, countSteps());
        xml::Node testcase("testcase");
        try {
          testcase.attr("classname") =
            xmlattr(_testclass.size()
                    ?_testclass
                    :"file."+(*cmd)->file()
                    .replace(QRegularExpression(".wt$"), "")
                    .replace(".", "-"), true)
            .toStdString();
          testcase.attr("name") = 
            xmlattr(QString("%1: %2")
                    .arg((*cmd)->line())
                    .arg((*cmd)->command().split('\n').takeFirst(), true))
            .toStdString();
          if (!_ignores.size() || (*cmd)->tag()=="label") { // not ignored
            _timer.start(_timeout*1000);
            try {
              command(*cmd);
              if (!(res=(*cmd)->execute(this, frame))) {
                _timer.stop();
                if (!back) retries = 0; else --back;
                testcase<<(xml::String("system-out") =
                           xmlattr(_cout).toStdString());
                testcase<<(xml::String("system-err") =
                           xmlattr(_cerr).toStdString());
                _cout.clear();
                _cerr.clear();
                _testsuites->last()<<testcase;
                break; // test is successfully finished
              }
              progress((*cmd)->file(), (*cmd)->line(), _step, countSteps());
            } catch (PossibleRetryLoad& e) {
              _timer.stop();
              // timeout may happen during load due to bad internet connection
              if (screenshots)
                try { // take a screenshot on error
                  Logger log(0, this);
                  QString filename(Screenshot::screenshot
                                   (log, (*cmd)->line(), targetdir(),
                                    _testclass,
                                    QString("retry-%1")
                                    .arg((ulong)retries, 2, 10,
                                         QLatin1Char('0')),
                                    frame));
                  plainlog("[[ATTACHMENT|"+filename+"]]");
                } catch (... ) {} // ignore exception in screenshot
              if (++retries<=maxretries) { // retry in that case
                QUrl url(frame->url());
                if ((*cmd)->command()=="expect loadFinished true") {
                  ------cmd;
                  ------_step;
                  back += 3;
                  _ignoreSignalsUntil = "loadStarted";
                  frame->load(url);
                } else if ((*cmd)->command()=="expect loadStarted") {
                  ----cmd;
                  ----_step;
                  back += 2;
                  _ignoreSignalsUntil = "loadStarted";
                  frame->page()->triggerAction(QWebPage::Stop);
                } else if ((*cmd)->command().startsWith("expect urlChanged")) {
                  QString url2((*cmd)->command());
                  url2.remove("expect urlChanged");
                  if (url2.size()) url=url2.trimmed();
                  ----cmd;
                  ----_step;
                  back += 2;
                  _ignoreSignalsUntil = "loadStarted";
                  frame->load(url);
                } else if ((*cmd)->command().startsWith("expect load")) {
                  QString url2((*cmd)->command());
                  url2.remove("expect load");
                  if (url2.size()) url=url2.trimmed();
                  ----cmd;
                  ----_step;
                  back += 2;
                  _ignoreSignalsUntil = "loadStarted";
                  frame->load(url);
                } else {
                  throw;
                }
              } else {
                throw;
              }
              log(QString("WARNING: retry#%1, redo last %2 steps; error: %3")
                  .arg(retries).arg(back).arg(e.what()));
            }
            _timer.stop();
            if (!back) retries = 0; else --back;
            testcase<<(xml::String("system-out") =
                       xmlattr(_cout).toStdString());
            testcase<<(xml::String("system-err") =
                       xmlattr(_cerr).toStdString());
            _cout.clear();
            _cerr.clear();
            if ((*cmd)->isTestcase())
              _testsuites->last()<<testcase;
          }
        } catch (Exception& e) {
          _timer.stop();
          if (!back) retries = 0; else --back;
          testcase<<(xml::String("system-out") =
                     xmlattr(_cout).toStdString());
          testcase<<(xml::String("system-err") =
                     xmlattr(_cerr).toStdString());
          _cout.clear();
          _cerr.clear();
          if ((*cmd)->isTestcase())
            _testsuites->last()<<testcase;
          removeSignals(frame);
          e.prependFileLine((*cmd)->file(), (*cmd)->line());
          if (screenshots)
            try { // write html source and take a last screenshot on error
              {
                Logger log(0, this);
                QString filename(Screenshot::sourceHtml
                                 (log, (*cmd)->line(), targetdir(),
                                  _testclass,
                                  "error", frame));
                plainlog("[[ATTACHMENT|"+filename+"]]");
              } {
                Logger log(0, this);
                QString filename(Screenshot::screenshot
                                 (log, (*cmd)->line(), targetdir(),
                                  _testclass,
                                  "error", frame));
                plainlog("[[ATTACHMENT|"+filename+"]]");
              }
            } catch (... ) {} // ignore exception in screenshot
          throw;
        }
      }
      removeSignals(frame);
      if (!_signals.empty()) error(UnhandledSignals(_signals));
      progress(QString(), 0, 0, 0);
      return res;
    }
    std::shared_ptr<Command> command() {
      return _command;
    }
    void command(std::shared_ptr<Command> cmd) { // maintained by Logger
      _command = cmd;
      if (_parent) _parent->command(cmd);
    }
    void parent(Script* p) {
      _parent = p;
    }
    QString& cout() {
      return _cout;
    }
    QString& cerr() {
      return _cerr;
    }
    int countSteps() {
      int res(0);
      for (auto cmd(_script.begin()); cmd!=_script.end(); ++cmd)
        res += (*cmd)->steps(this);
      return res;
    }
    bool screenshots() {
      return _screenshots;
    }
    void targetdir(QString name) {
      _targetdir = name;
    }
    QString targetdir() {
      return _targetdir;
    }
    void testclass(QString tc) {
      _testclass = tc;
    }
    QString testclass() {
      return _testclass;
    }
    void testsuite(QString name) {
      xml::Node testsuite("testsuite");
      testsuite.attr("name") = xmlattr(name, true).toStdString();
      testsuite.attr("timestamp") =
        QDateTime::currentDateTime().toString(Qt::ISODate).toStdString();
      _testsuites->last().attr("failures") = "0";
      (*_testsuites)<<testsuite;
    }
    QStringList getSignals() {
      QStringList res;
      for (std::queue<Signal> sigs(_signals); !sigs.empty(); sigs.pop())
        res+=sigs.front().first;
      return res;
    }
    Signal getSignal() {
      while (_signals.empty()) QCoreApplication::processEvents();
      Signal res(_signals.front());
      _signals.pop();
      return res;
    }
    bool checkSignal(const QString& name, const QStringList& args = QStringList()) {
      if (_ignore.contains(name)) {
        log(QString("ignoring signal check for %1").arg(name));
        return true;
      }
      while (_signals.empty()) QCoreApplication::processEvents();
      Signal res(_signals.front());
      if (res.first==name && (args.empty() || res.second==args)) {
        _signals.pop();
        return true;
      }
      return false;
    }
    QTimer& timer() {
      return _timer;
    }
    void ignore(const Script& o) {
      _ignore = o._ignore;
    }
    bool ignores(const QString& sig = QString()) {
      if (sig.isEmpty()) return !_ignore.empty();
      return _ignore.contains(sig);
    }
    void ignore(QStringList sigs = QStringList()) {
      if (sigs.empty())
        sigs<<"loadFinished"<<"loadStarted"<<"frameChanged"<<"titleChanged"<<"urlChanged";
      for (const QString& sig: sigs) {
        log("start ignoring: '"+sig+"'");
        _ignore<<sig;
      }
    }
    void unignore(QStringList sigs = QStringList()) {
      if (sigs.empty())
        sigs<<"loadFinished"<<"loadStarted"<<"frameChanged"<<"titleChanged"<<"urlChanged";
      for (const QString& sig: sigs) {
        if (_ignore.contains(sig)) {
          log("stop ignoring: '"+sig+"'");
          _ignore.erase(_ignore.find(sig));
        }
      }
    }
    void ignoreto(const QString& l) {
      _ignores.insert(l);
    }
    void label(const QString& l) {
      _ignores.remove(l);
    }
    void set(QString name, QString value) {
      _variables[name] = value;
      _rvariables[value] = name;
    }
    QStringList variables() {
      return _variables.keys();
    }
    QString variable(QString name) {
      QMap<QString, QString>::iterator it(_variables.find(name));
      if (it==_variables.end()) error(VariableNotFound(name));
      return *it;
    }
    /// Copy context from other script
    void set(const Script& o) {
      _variables = o._variables;
      _rvariables = o._rvariables;
      _timeout = o._timeout;
      _defaultTimeout = o._timeout;
      _clicktype = o._clicktype;
      _testsuites = o._testsuites;
      _testclass = o._testclass;
      _targetdir = o._targetdir;
      _ignore = o._ignore;
      _ignores = o._ignores;
      _maxretries = o._maxretries;
      _screenshots = o._screenshots;
      _cout.clear();
      _cerr.clear();
      _ignoreSignalsUntil.clear();
      _functions.unite(o._functions);
    }
    void unset(QString name) {
      _rvariables.remove(_variables[name]);
      _variables.remove(name);
    }
    QStringList functions() {
      return _functions.keys();
    }
    void function(QString name, std::shared_ptr<Function> f) {
      _functions[name] = f;
    }
    std::shared_ptr<Function> function(QString name) {
      QMap<QString, std::shared_ptr<Function> >::iterator
        it(_functions.find(name));
      if (it==_functions.end()) error(FunctionNotFound(name));
      return *it;
    }
    void timeout(int t) {
      _timeout = t;
    }
    int timeout() {
      return _timeout;
    }
    void defaultTimeout(int t) {
      _defaultTimeout = t;
    }
    void auth(const QString& realm, const QString& username, const QString& password) {
      if (!username.isEmpty() && !password.isEmpty())
        _auth[realm] = {username, password};
      else if (_auth.contains(realm))
        _auth.erase(_auth.find(realm));
    }
    void clicktype(ClickType c) {
      _clicktype = c;
    }
    ClickType clicktype() {
      return _clicktype;
    }
    QString replacevars(QString txt) {
      for (QMap<QString, QString>::iterator it(_variables.begin());
           it!=_variables.end(); ++it)
        txt.replace(it.key(), it.value(), Qt::CaseSensitive);
      return txt;
    }
    QStringList replacevars(QStringList txts) {
      for (QStringList::iterator txt(txts.begin()); txt!=txts.end(); ++txt)
        *txt = replacevars(*txt);
      return txts;
    }
    QString insertvars(QString txt) {
      QMapIterator<LenString, LenString> it(_rvariables);
      it.toBack();
      while (it.hasPrevious()) {
        it.previous();
        txt.replace(it.key(), it.value(), Qt::CaseSensitive);
      }
      return txt;
    }
    QString result() {
      if (_script.size()) return (*_script.rbegin())->result();
      return QString();
    }
    void addSignals(QWebFrame* frame) {
      assert(connect(dynamic_cast<NetworkAccessManager*>
              (frame->page()->networkAccessManager()),
              SIGNAL(log(QString)),
                     SLOT(log(QString))));
      assert(connect(frame->page()->networkAccessManager(),
              SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)),
                     SLOT(authenticationRequired(QNetworkReply*, QAuthenticator*))));
      assert(connect(frame, SIGNAL(contentsSizeChanged(const QSize&)),
                     SLOT(contentsSizeChanged(const QSize&))));
      assert(connect(frame, SIGNAL(iconChanged()),
                     SLOT(iconChanged())));
      assert(connect(frame, SIGNAL(initialLayoutCompleted()),
                     SLOT(initialLayoutCompleted())));
      assert(connect(frame, SIGNAL(javaScriptWindowObjectCleared()),
                     SLOT(javaScriptWindowObjectCleared())));
      assert(connect(frame, SIGNAL(loadFinished(bool)),
                     SLOT(loadFinished(bool))));
      assert(connect(frame, SIGNAL(loadStarted()),
                     SLOT(loadStarted())));
      assert(connect(frame, SIGNAL(titleChanged(const QString&)),
                     SLOT(titleChanged(const QString&))));
      assert(connect(frame, SIGNAL(urlChanged(const QUrl&)),
                     SLOT(urlChanged(const QUrl&))));
      assert(connect(&_timer, SIGNAL(timeout()), SLOT(timedout())));
    }
    void removeSignals(QWebFrame* frame) {
      disconnect(dynamic_cast<NetworkAccessManager*>
                 (frame->page()->networkAccessManager()),
                 SIGNAL(log(QString)),
                 this, SLOT(log(QString)));
      disconnect(frame->page()->networkAccessManager(),
                 SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)),
                 this, SLOT(authenticationRequired(QNetworkReply*, QAuthenticator*)));
      disconnect(frame, SIGNAL(contentsSizeChanged(const QSize&)),
                 this, SLOT(contentsSizeChanged(const QSize&)));
      disconnect(frame, SIGNAL(iconChanged()),
                 this, SLOT(iconChanged()));
      disconnect(frame, SIGNAL(initialLayoutCompleted()),
                 this, SLOT(initialLayoutCompleted()));
      disconnect(frame, SIGNAL(javaScriptWindowObjectCleared()),
                 this, SLOT(javaScriptWindowObjectCleared()));
      disconnect(frame, SIGNAL(loadFinished(bool)),
                 this, SLOT(loadFinished(bool)));
      disconnect(frame, SIGNAL(loadStarted()),
                 this, SLOT(loadStarted()));
      disconnect(frame, SIGNAL(titleChanged(const QString&)),
                 this, SLOT(titleChanged(const QString&)));
      disconnect(frame, SIGNAL(urlChanged(const QUrl&)),
                 this, SLOT(urlChanged(const QUrl&)));
      disconnect(frame, SIGNAL(urlChanged(const QUrl&)),
                 this, SLOT(urlChanged(const QUrl&)));
      disconnect(&_timer, SIGNAL(timeout()), this, SLOT(timedout()));
    }
  public Q_SLOTS:
    void log(QString text, Command* command = 0) {
      QString prefix
        (QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss "));
      Command* cmd(command?command:_command.get());
      for (QChar& c: text) if (c<32&&c!='\n') c='?';
      if (cmd)
        prefix += QString("%2:%3%1 ")
          .arg(QString(cmd->indent()+2, QChar(' ')))
          .arg(cmd->file(), 20, QChar(' '))
          .arg(cmd->line(), -4, 10, QChar(' '));
      else
        prefix += "    .... ";
      text = prefix+text.split('\n').join("\n"+prefix+"    ");
      logging(text);
      std::cout<<text<<std::endl<<std::flush;
      _cout += text + "\n";
    }
    void plainlog(QString text) {
      logging(text);
      std::cout<<text<<std::endl<<std::flush;
      _cout += text + "\n";
    }
    void parentlog(QString text) {
      logging(text);
      _cout += text + "\n";
    }
  private:
    void error(const Exception& e) {
      log(QString("  FAILED[")+demangle(typeid(e).name())+"]: "+e.what());
      throw e;
    }
    std::shared_ptr<Command> unknown(QString command) {
      if (!command.size())
        return std::shared_ptr<Command>(new Empty());
      if (command[0]=='#')
        return std::shared_ptr<Command>(new Comment(command));
      throw UnknownCommand(command); // error
    }
    void initPrototypes();
    void add(Command* c) {
      _prototypes[c->tag()] = std::shared_ptr<Command>(c);
    }
  private Q_SLOTS:
    void innerProgress(QString file, int line, int delta) {
      progress(file, line, _step+delta, countSteps());
    }
    void authenticationRequired(QNetworkReply*, QAuthenticator* a) {
      if (_auth.contains(a->realm())) {
        log("network: login to "+a->realm());
        a->setUser(_auth[a->realm()].username);
        a->setPassword(_auth[a->realm()].password);
      } else {
        log("network: no credentials for "+a->realm());
      }
    }
    void contentsSizeChanged(const QSize&) {
    }
    void iconChanged() {
    }
    void initialLayoutCompleted() {
    }
    void javaScriptWindowObjectCleared() {
    }
    void loadFinished(bool ok) {
      if (_ignore.contains("loadFinished")) {
        log("ignored loadFinished");
        return;
      }
      QString sig(ok?"true":"false");
      if (_ignoreSignalsUntil.size() &&
          _ignoreSignalsUntil != "loadFinished "+sig) {
        log("warning: ignored loadFinished, waiting for "+_ignoreSignalsUntil);
        return;
      }
      _ignoreSignalsUntil.clear();
      log("  signal received: loadFinished "+QString(ok?"true":"false"));
      _signals.push(std::make_pair("loadFinished", QStringList(sig)));
    }
    void loadStarted() {
      if (_ignore.contains("loadStarted")) {
        log("ignored loadStarted");
        return;
      }
      if (_ignoreSignalsUntil.size() && _ignoreSignalsUntil != "loadStarted") {
        log("warning: ignored loadStarted, waiting for "+_ignoreSignalsUntil);
        return;
      }
      _ignoreSignalsUntil.clear();
      log("  signal received: loadStarted");
      _signals.push(std::make_pair("loadStarted", QStringList()));
    }
    void frameChanged() {
      if (_ignore.contains("frameChanged")) {
        log("ignored frameChanged");
        return;
      }
    }
    void titleChanged(const QString&) {
      if (_ignore.contains("titleChanged")) {
        log("ignored titleChanged");
        return;
      }
      //_signals.push(std::make_pair("titleChanged", QStringList(title)));
    }
    void urlChanged(const QUrl& url) {
      if (_ignore.contains("urlChanged")) {
        log("ignored urlChanged");
        return;
      }
      if (_ignoreSignalsUntil.size() && _ignoreSignalsUntil != "urlChanged") {
        log("warning: ignored urlChanged, waiting for "+_ignoreSignalsUntil);
        return;
      }
      _ignoreSignalsUntil.clear();
      log("  signal received: urlChanged "+url.toString());
      _signals.push(std::make_pair("urlChanged",
                                   QStringList(url.toString())));
    }
    void timedout() {
      error(TimeOut(_command->command()));
    }
  private:
    struct AuthRealm {
        QString username;
        QString password;
    };
    typedef std::map<QString, std::shared_ptr<Command>> Prototypes;
    typedef std::vector<std::shared_ptr<Command>> Commands;
    Prototypes _prototypes;
    Commands _script;
    std::queue<Signal> _signals;
    QSet<QString> _ignore; ///< signals to ignore
    QTimer _timer;
    int _step;
    QSet<QString> _ignores;
    QString _cout;
    QString _cerr;
    bool _screenshots;
    int _maxretries;
    QString _ignoreSignalsUntil;
    QMap<QString, QString> _variables; ///< variable mapping
    QMap<LenString, LenString> _rvariables; ///< reverse variable mapping
    QMap<QString, std::shared_ptr<Function> > _functions;
    int _timeout;
    int _defaultTimeout;
    ClickType _clicktype;
    QString _targetdir;
    std::shared_ptr<xml::Node> _testsuites; ///< only valid within run
    QString _testclass;
    std::shared_ptr<Command> _command;
    QString _path;
    QString _filename;
    QMap<QString, AuthRealm> _auth;
    Script* _parent = nullptr;
};

class CommandContainer: public Command {
  public:
    int steps(Script* parent) {
      return countSteps(parent);
    }
  protected:
    virtual int countSteps(Script*) const {
      return _script->countSteps()+1;
    }
    std::shared_ptr<Script> _script;
};

class Do: public Command {
  public:
    QString tag() const {
      return "do";
    }
    QString description() const {
      return
        tag()+" [<selector>]\n  <javascript-line1>\n  <javascript-line2>"
        "\n\n"
        "Execute JavaScript on a CSS selected object. The object is the first "
        "object in the DOM tree that matches the given CSS selector. You can "
        "refere to the selected object within the scripy by \"this\". The "
        "JavaScript code is on the following lines and at least intended by "
        "one space";
    }
    QString command() const {
      return tag()+" "+_selector+(_javascript.size()?"\n"+_javascript:"");
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList& in, QString, int, int) {
      std::shared_ptr<Do> cmd(new Do());
      cmd->_selector = args.trimmed();
      cmd->_javascript = subCommandBlock(in).join("\n");
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      QWebElement element(frame->documentElement());
      if (_selector.size()) {
        element = find(frame, script->replacevars(_selector), script->timeout()-1);
        if (element.isNull())
          error(log, ElementNotFound(script->replacevars(_selector)));
      }
      _result =
        element.evaluateJavaScript(script->replacevars(_javascript)).toString();
      log("result: "+(_result.size()?_result:"(void)"));
      return true;
    }
  private:
    QString _selector;
    QString _javascript;
};

class Load: public Command {
  public:
    QString tag() const {
      return "load";
    }
    QString description() const {
      return
        tag()+" <url>"
        "\n\n"
        "Load an URL, the URL is given as parameter in full syntax.";
    }
    QString command() const {
      return tag()+" "+_url;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Load> cmd(new Load());
      cmd->_url = args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      frame->load(script->replacevars(_url));
      return true;
    }
  private:
    QString _url;
};
    
class Expect: public Command {
  public:
    QString tag() const {
      return "expect";
    }
    QString description() const {
      return
        tag()+" <signal> [<parameter>]"
        "\n\n"
        "Expect a signal. Signals are emitted by webkit and may contain "
        "parameter. If a parameter is given in the script, then the parameter "
        "must match exactly. If no parameter is given, then the signal must "
        "be emitted, but the parameters of the signal are not checked."
        "\n\n"
        "Known signals and parameters are:\n"
        " - loadFinished <bool: \"true\" if ok, \"false\" on error>\n"
        " - loadStarted\n"
        " - urlChanged <url>\n"
        "There is a short hand:\n"
        " - load <url>\n"
        "   stands for the three signals:\n"
        "    - loadStarted (optional)\n"
        "    - urlChanged <url>\n"
        "    - loadFinished true";
    }
    QString command() const {
      return tag()+" "+_signal._signal
        +(_signal._args.size()?" "+_signal._args.join(' '):QString());
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Expect> cmd(new Expect());
      cmd->_signal._args = args.split(" ");
      cmd->_signal._signal = cmd->_signal._args.takeFirst();
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      QStringList args(script->replacevars(_signal._args));
      if (_signal._signal=="load") { // special signal load
        while (!script->ignores("loadStarted") && script->checkSignal("loadStarted")) {
          log("ignore signal: loadStarted"); // ignore optional loadStarted
        }
        if (!script->checkSignal("urlChanged", args))
          error(log, WrongSignal("urlChanged", args, script->getSignal(),
                                 script->getSignals()));
        args=QStringList("true");
        if (!script->checkSignal("loadFinished", args))
          error(log, WrongSignal("loadFinished", args, script->getSignal(),
                                 script->getSignals()));
      } else {
        if (!script->checkSignal(_signal._signal, args))
          error(log, WrongSignal(_signal._signal, args, script->getSignal(),
                                 script->getSignals()));
      }
      return true;
    }
  private:
    struct Signal {
        Signal(QString s, QStringList a): _signal(s), _args(a) {}
        Signal(QString s, QString a): _signal(s), _args(a) {}
        Signal(QString s): _signal(s) {}
        Signal() {}
        QString _signal;
        QStringList _args;
    };
    Signal _signal;
};

class Open: public Command {
  public:
    QString tag() const {
      return "open";
    }
    QString description() const {
      return
        tag()+
        "\n\n"
        "Open the browser window, so you can follow the test steps visually.";
    }
    QString command() const {
      return tag();
    }
    std::shared_ptr<Command> parse(Script*, QString,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Open> cmd(new Open());
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      frame->page()->view()->show();
      return true;
    }
};

class Sleep: public Command {
  public:
    QString tag() const {
      return "sleep";
    }
    QString description() const {
      return
        tag()+" <seconds>"
        "\n\n"
        "Sleep for a certain amount of seconds. This helps, if you must wait "
        "for some javascript actions, i.e. AJAX or slow pages, and the "
        "excpeted signals are not sufficient.";
    }
    QString command() const {
      return tag()+" "+_time;
    }
    std::shared_ptr<Command> parse(Script*, QString time,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Sleep> cmd(new Sleep());
      cmd->_time = "10"; // default: 10s
      if (time.size()) cmd->_time = time;
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      script->timer().stop();
      bool ok;
      unsigned int time(script->replacevars(_time).toUInt(&ok));
      if (!ok)
        error(log, BadArgument(script->replacevars(_time)
                               +" should be a number of seconds"));
      sleep(time);
      return true;
    }
  private:
    QString _time;
};

class Exit: public Command {
  public:
    QString tag() const {
      return "exit";
    }
    QString description() const {
      return
        tag()+
        "\n\n"
        "Successfully terminate script immediately. The following commands "
        "are not executed. This helps when you debug your scripts and you "
        "want the script stop at a certain point for investigations.";
    }
    QString command() const {
      return tag();
    }
    std::shared_ptr<Command> parse(Script*, QString,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Exit> cmd(new Exit());
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      return false;
    }
};

class IgnoreTo: public Command {
  public:
    QString tag() const {
      return "ignoreto";
    }
    QString description() const {
      return
        tag()+" <label>"
        "\n\n"
        "Ignore all following commands up to a given label. The following "
        "commands are not executed until the given label appears in the "
        "script. This helps when you debug your scripts and you "
        "want to skip some lines of script code.";
    }
    QString command() const {
      return tag()+" "+_label;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<IgnoreTo> cmd(new IgnoreTo());
      if (!args.size()) throw BadArgument("ignoreto needs a label");
      cmd->_label=args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      script->ignoreto(script->replacevars(_label));
      return true;
    }
  private:
    QString _label;
};

class Label: public Command {
  public:
    QString tag() const {
      return "label";
    }
    QString description() const {
      return
        tag()+" <label>"
        "\n\n"
        "This marks the label refered by command \"ignoreto\".";
    }
    QString command() const {
      return tag()+" "+_label;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Label> cmd(new Label());
      if (!args.size()) throw BadArgument("label needs a label as parameter");
      cmd->_label=args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      script->label(script->replacevars(_label));
      return true;
    }
  private:
    QString _label;
};

class Upload: public Command {
  public:
    QString tag() const {
      return "upload";
    }
    QString description() const {
      return
        tag()+" <selector> -> <filename>"
        "\n\n"
        "Presses the specified file upload button and passes a given file "
        "name. The command requires a CSS selector followed by a filename. "
        "The first object that matches the selector is used.";
    }
    QString command() const {
      return tag()+" "+_selector+" -> "+_filename;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Upload> cmd(new Upload());
      QStringList allargs(args.split("->"));
      if (allargs.size()<2)
        throw BadArgument(tag()+"requires <selector> -> <filename>, "
                               "instead of: \""+args+"\"");
      cmd->_selector = allargs.takeFirst().trimmed();
      cmd->_filename = allargs.join("->").trimmed();
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      TestWebPage* page(dynamic_cast<TestWebPage*>(frame->page()));
      assert(page);
      QString filename(script->path()+script->replacevars(_filename));
      if (!QFileInfo(filename).exists()) {
        QStringList files(QFileInfo(filename).dir()
                          .entryList(QStringList(filename)));
        if (files.size()==1) filename=files[0];
      }
      if (!QFileInfo(filename).exists()) filename=script->replacevars(_filename);
      if (!QFileInfo(filename).exists()) {
        QStringList files(QFileInfo(filename).dir()
                          .entryList(QStringList(filename)));
        if (files.size()==1) filename=files[0];
      }
      page->setNextUploadFile(filename);
      realMouseClick(log, frame, script, script->replacevars(_selector));
      if (page->uploadPrepared())
        error(log, SetFileUploadFailed(script->replacevars(_selector), filename));
      return true;
    }
  private:
    QString _selector;
    QString _filename;
};

class Exists: public Command {
  public:
    QString tag() const {
      return "exists";
    }
    QString description() const {
      return
        tag()+" <selector> -> <text>"
        "\n\n"
        "Assert that a certain text exists in the selected object, or if no "
        "text is given, assert that the specified object exists. The object "
        "is given by a CSS selector. All matching objects are search for the "
        "text.";
    }
    QString command() const {
      return tag()+" "+_selector+(_text.size()?" -> "+_text:QString());
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Exists> cmd(new Exists());
      QStringList allargs(args.split("->"));
      if (allargs.size()<2) {
        cmd->_selector = args;
      } else {
        cmd->_selector = allargs.takeFirst().trimmed();
        cmd->_text = allargs.join("->").trimmed();
      }
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      QString selector(script->replacevars(_selector));
      QString text(script->replacevars(_text));
      QStringList notfound;
      QWebElement firstelement(find(frame, selector, script->timeout()-1));
      for (QWebElement element: frame->findAllElements(selector)) {
        if (text.isEmpty()) return true; // just find element
        if (element.toOuterXml().indexOf(text)!=-1) return true;
        if (element.toPlainText().indexOf(text)!=-1) return true;
        notfound += element.toOuterXml();
      }
      if (text.isEmpty())
        error(log, AssertionFailed("element not found: "+selector));
      else if (firstelement.isNull())
        error(log, AssertionFailed("expected \""+text+"\" in non existing element "
                                   +selector));
      else
        error(log, AssertionFailed("not found \""+text+"\" in \""
                                   +notfound.join("\", \"")+"\" on "+selector));
      return true; // never reached due to error, above
    }
  private:
    QString _selector;
    QString _text;
};

class Not: public Command {
  public:
    QString tag() const {
      return "not";
    }
    QString description() const {
      return
        tag()+" <selector> -> <text>"
        "\n\n"
        "Assert that a certain text does not exists in the selected object, "
        "or if no text is given, assert that the specified object does not "
        "exists. The object is given by a CSS selector. All matching objects "
        "are search for the text.";
    }
    QString command() const {
      return tag()+" "+_selector+(_text.size()?" -> "+_text:QString());
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Not> cmd(new Not());
      QStringList allargs(args.split("->"));
      if (allargs.size()<2) {
        cmd->_selector = args;
      } else {
        cmd->_selector = allargs.takeFirst().trimmed();
        cmd->_text = allargs.join("->").trimmed();
      }
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      QString selector(script->replacevars(_selector));
      QString text(script->replacevars(_text));
      QWebElement firstelement(find(frame, selector,
                                    mrw::max(mrw::min(script->timeout()/3, 10), 2)));
      for (QWebElement element: frame->findAllElements(selector)) {
        if (text.isEmpty())
          error(log, AssertionFailed("element must not exists: "+selector));
        if (element.toOuterXml().indexOf(text)!=-1)
          error(log, AssertionFailed("\""+text+"\" must not be in \""
                                     +element.toInnerXml()+"\" on "+selector));
      }
      return true;
    }
  private:
    QString _selector;
    QString _text;
};

class Execute: public Command {
  public:
    QString tag() const {
      return "execute";
    }
    QString description() const {
      return
        tag()+" <command>\n  <line1>\n  <line2>\n  <...>"
        "\n\n"
        "Execute <command>. The command can have space separated arguments. "
        "Following lines that are intended by at least "
        "one space are streamed into the command. This way, you can e.g. "
        "execute a bash script to check a file you downloaded from a page.";
    }
    QString command() const {
      QStringList script(_script);
      script.replaceInStrings(QRegularExpression("^"), "  ");
      return tag()+" "+_command
        +(_args.size()?" "+_args.join(' '):QString())
        +(script.size()?"\n  "+script.join("\n  "):QString());
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList& in, QString, int, int) {
      std::shared_ptr<Execute> cmd(new Execute());
      cmd->_args = args.split(' ');
      cmd->_command = cmd->_args.takeFirst();
      cmd->_script = subCommandBlock(in);
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      script->timer().stop();
      QString command(script->replacevars(_command));
      QStringList args;
      QString scripttxt(script->replacevars(_script.join("\n")));
      for (QString arg: _args) args.push_back(script->replacevars(arg));
      QProcess exec;
      exec.setProcessChannelMode(QProcess::MergedChannels);
      exec.start(command, args);
      if (!exec.waitForStarted())
        error(log, CannotStartScript(command, args, scripttxt));
      if (scripttxt.size()) {
        if (exec.write(scripttxt.toUtf8())!=scripttxt.toUtf8().size() ||
            !exec.waitForBytesWritten(60000))
          error(log, CannotLoadScript(command, args, scripttxt));
      }
      exec.closeWriteChannel();
      if (!exec.waitForFinished(60000) && exec.state()!=QProcess::NotRunning)
        error(log, ScriptNotFinished(command, args, scripttxt));
      QString sout(exec.readAllStandardOutput());
      QString serr(exec.readAllStandardError());
      _result = sout;
      log("result: "+(_result.size()?_result:"(void)"));
      script->log(sout);
      if (exec.exitCode()!=0 || exec.exitStatus()!=QProcess::NormalExit)
        error(log, ScriptExecutionFailed(command, args, scripttxt,
                                         exec.exitCode(), sout, serr));
      return true;
    }
  private:
    QString _command;
    QStringList _args;
    QStringList _script;
};

class Download: public Command {
    Q_OBJECT
  public:
    QString tag() const {
      return "download";
    }
    QString description() const {
      return
        tag()+" <filename>\n"
        "  <command-to-start-download>"
        "\n\n"
        "Set download file before loading a download link or clicking on a "
        "download button. The next line must be exactly one command that "
        "initiates the download.\n\n"
        "Please note that variables are not substituted in the filename.";
    }
    QString command() const {
      return tag()+(_filename.size()?" "+_filename:QString())+"\n"
        +_next->command();
    }
    std::shared_ptr<Command> parse(Script* script, QString args,
                                   QStringList& in, QString file, int line,
                                   int indent) {
      std::shared_ptr<Download> cmd(new Download());
      cmd->_filename = args.trimmed();
      cmd->_next = script->parseLine(in, file, line+1, indent+1);
      cmd->_next->log(false); // suppress logging of subcommand
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      _realfilename = script->replacevars(_filename);
      log("REALFILENAME="+_realfilename);
      frame->page()->setForwardUnsupportedContent(true);
      assert(connect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)),
                     this, SLOT(unsupportedContent(QNetworkReply*))));
      try {
        bool res(_next->execute(script, frame)); // start download
        script->timer().stop(); // no timeout during download
        for (_done = false; !_done;) // wait for download finish
          QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
        log("download terminated "+
            QString(_netsuccess&&_filesuccess?"successfully":"with error"));
        if (!_netsuccess) error(log, DownloadFailed(_realfilename));
        if (!_filesuccess) error(log, WriteFileFailed(_realfilename));
        log["[[ATTACHMENT|"+QDir(_realfilename).absolutePath()+"]]"];
        disconnect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)),
                   this, SLOT(unsupportedContent(QNetworkReply*)));
        return res;
      } catch (...) {
        disconnect(frame->page(), SIGNAL(unsupportedContent(QNetworkReply*)),
                   this, SLOT(unsupportedContent(QNetworkReply*)));
        throw;
      }
    }
  private Q_SLOTS:
    void completed(bool netsuccess, bool filesuccess) {
      _done = true;
      _netsuccess = netsuccess;
      _filesuccess = filesuccess;
    }
    void unsupportedContent(QNetworkReply* reply) {
      if (!_realfilename.size()) {
        _realfilename = reply->url().toString().split('/').last();
        if (reply->header(QNetworkRequest::ContentDispositionHeader).isValid()) {
          QString part(reply->header(QNetworkRequest::ContentDispositionHeader)
                       .toString());
          if (part.contains(QRegularExpression("attachment; *filename="))) {
            part.replace(QRegularExpression(".*attachment; *filename="), "");
            if (part.size()) _realfilename = part;
          }
        }
      }
      assert(connect(new RunDownload(reply, _realfilename),
                     SIGNAL(completed(bool, bool)), SLOT(completed(bool, bool))));
    }
  private:
    QString _filename;
    QString _realfilename;
    std::shared_ptr<Command> _next;
    bool _done, _netsuccess, _filesuccess;
};

class Click: public Command {
  public:
    QString tag() const {
      return "click";
    }
    QString description() const {
      return
        tag()+" [<clicktype>] <selector>"
        "\n\n"
        "Click on the specified element. Either you explicitely specify a click"
        " type, such as <realmouse> or <javascript>, or the previously set or"
        " the default clicktype is used.";
    }
    QString command() const {
      return tag()+(_clicktype
                    ?(*_clicktype==Script::REAL_MOUSE_CLICK?" realmouse"
                      :*_clicktype==Script::JAVASCRIPT_CLICK?" javascript":"")
                    :"")+" "+_selector;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Click> cmd(new Click());
      if (args.trimmed().contains(QRegularExpression("^realmouse ")))
        cmd->_clicktype = Script::REAL_MOUSE_CLICK;
      else if (args.trimmed().contains(QRegularExpression("^javascript ")))
        cmd->_clicktype = Script::JAVASCRIPT_CLICK;
      cmd->_selector =
        args.remove(QRegularExpression("^ *(realmouse|javascript) +"));
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      QString clicktarget(script->replacevars(_selector));
      switch (_clicktype ? *_clicktype : script->clicktype()) {
        case Script::REAL_MOUSE_CLICK: {
          realMouseClick(log, frame, script, clicktarget);
          break;
        }
        case Script::JAVASCRIPT_CLICK:
        default: {
          QWebElement element(find(frame, clicktarget, script->timeout()-1));
          if (element.isNull())
            error(log, ElementNotFound(clicktarget));
          _result = element.evaluateJavaScript("this.click();").toString();
          if (_result.size()) log("result: "+_result.size());
          break;
        }
      }
      return true;
    }
  private:
    QString _selector;
    std::optional<Script::ClickType> _clicktype;
};

class Set: public Command {
  public:
    QString tag() const {
      return "set";
    }
    QString description() const {
      return
        tag()+" <variable>=<value>\n"+
        tag()+" <variable>\n"
        "  <command>"
        "\n\n"
        "Sets the value of a variable either to a constant, or to the output"
        " of a command. A command should be a command that produces an"
        " output, such as <do>, which returns the result of JavaScript or"
        " <execute>, which returns the output of the executed command, or"
        " <call>, which returns the result of the last command."
        " All variables are global with regard to functions.";
    }
    QString command() const {
      if (_next)
        return tag()+" "+_name+"\n  "+_next->command();
      else
        return tag()+" "+_name+" = "+_value;
    }
    std::shared_ptr<Command> parse(Script* script, QString args,
                                   QStringList& in, QString file, int line,
                                   int indent) {
      std::shared_ptr<Set> cmd(new Set());
      cmd->_next = 0;
      QStringList allargs(args.split("="));
      cmd->_name = allargs.takeFirst().trimmed();
      cmd->_value = allargs.join("=").trimmed();
      if (!args.contains('=')) {
        cmd->_next = script->parseLine(in, file, line+1, indent+1);
        cmd->_next->log(false); // suppress logging of subcommand
      }
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      if (_next) {
        _next->execute(script, frame);
        script->set(_name,
                    script->replacevars(_next->result()));
        log(_name+"='"+_next->result().replace("\\", "\\\\").replace("'", "\\'")+"'");
      } else {
        script->set(_name,
                    script->replacevars(_value));
        log(_name+"='"+_value.replace("\\", "\\\\").replace("'", "\\'")+"'");
      }
      return true;
    }
  private:
    QString _name;
    QString _value;
    std::shared_ptr<Command> _next;
};

class UnSet: public Command {
  public:
    QString tag() const {
      return "unset";
    }
    QString description() const {
      return
        tag()+" <variable>"
        "\n\n"
        "Undo the setting of a variable. The opposite of «set».";
    }
    QString command() const {
      return tag()+" "+_name;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<UnSet> cmd(new UnSet());
      cmd->_name = args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      script->unset(_name);
      return true;
    }
  private:
    QString _name;
};

class Timeout: public Command {
  public:
    QString tag() const {
      return "timeout";
    }
    QString description() const {
      return
        tag()+" <seconds>"
        "\n\n"
        "Set the timeout in seconds (defaults to 10).";
    }
    QString command() const {
      return tag()+" "+_timeout;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Timeout> cmd(new Timeout());
      cmd->_timeout = args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      bool ok;
      int timeout(script->replacevars(_timeout).toInt(&ok));
      if (!ok) error(log, BadArgument(script->replacevars(_timeout)
                                      +" should be a number of seconds"));
      script->timeout(timeout);
      return true;
    }
  private:
    QString _timeout;
};

class CaCertificate: public Command {
  public:
    QString tag() const {
      return "ca-certificate";
    }
    QString description() const {
      return
        tag()+" <filename.pem>"
        "\n\n"
        "Load a CA certificate that will be accepted on SSL connections.";
    }
    QString command() const {
      return tag()+" "+_filename;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<CaCertificate> cmd(new (CaCertificate));
      cmd->_filename = args.trimmed();
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      QString filename(script->replacevars(_filename));
      QFile cacertfile(script->path()+filename);
      if (!cacertfile.exists()) cacertfile.setFileName(filename); // try without path
      if (!cacertfile.exists() || !cacertfile.open(QIODevice::ReadOnly))
        error(log, FileNotFound(filename));
      QSslCertificate cacert(&cacertfile);
      if (cacert.isNull()) error(log, NotACertificate(filename));
      QSslSocket::addDefaultCaCertificate(cacert);
      return true;
    }
  private:
    QString _filename;
};

class ClientCertificate: public Command {
  public:
    QString tag() const {
      return "client-certificate";
    }
    QString description() const {
      return
        tag()+" <certfile.pem> <keyfile.pem> <keypassword>"
        "\n\n"
        "Load a client certificate to authenticate on SSL connections. "
        "The password for the keyfile should not contain spaces. "
        "Create the two files from a PKCS#12 file using OpenSSL:\n"
        "  openssl pkcs12 -in certfile.p12 -out certfile.pem -nodes -clcerts\n"
        "  openssl pkcs12 -in certfile.p12 -out keyfile.pem -nocerts\n";
    }
    QString command() const {
      return tag()+" "+_certfile+" "+_keyfile+" "+_password;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<ClientCertificate> cmd(new (ClientCertificate));
      QStringList s(args.trimmed().split(' '));
      if (s.size()<3) throw MissingArguments(args, "certfile keyfile password");
      cmd->_certfile = s.takeFirst();
      cmd->_keyfile = s.takeFirst();
      cmd->_password = s.join(' ');
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      QSslConfiguration sslConfig(QSslConfiguration::defaultConfiguration());
      sslConfig.setProtocol(QSsl::AnyProtocol);
      sslConfig.setPeerVerifyMode(QSslSocket::AutoVerifyPeer);
      QString filename(script->replacevars(_certfile));
      QFile certfile(script->path()+filename);
      if (!certfile.exists()) certfile.setFileName(filename);
      if (!certfile.exists() || !certfile.open(QIODevice::ReadOnly))
        error(log, FileNotFound(filename));
      QSslCertificate cert(&certfile);
      if (cert.isNull()) error(log, NotACertificate(filename));
      sslConfig.setLocalCertificate(cert);
      filename = script->replacevars(_keyfile);
      QFile keyfile(script->path()+filename);
      if (!keyfile.exists()) keyfile.setFileName(filename);
      if (!keyfile.exists() || !keyfile.open(QIODevice::ReadOnly))
        error(log, FileNotFound(filename));
      keyfile.open(QIODevice::ReadOnly);
      QSslKey k(&keyfile, QSsl::Rsa, QSsl::Pem,
                QSsl::PrivateKey, script->replacevars(_password).toUtf8());
      if (k.isNull()) error(log, KeyNotReadable(filename));
      sslConfig.setPrivateKey(k);
      QSslConfiguration::setDefaultConfiguration(sslConfig);
      return true;
    }
  private:
    QString _certfile;
    QString _keyfile;
    QString _password;
};

class ClickType: public Command {
  public:
    QString tag() const {
      return "clicktype";
    }
    QString description() const {
      return
        tag()+" <type>"
        "\n\n"
        "Set how mouseclicks should be mapped. The best solution depends on"
        " your problem. Normally it is good to call \"click()\" on the element"
        " using javascript. But with complex javascript infected websites, it"
        " might be necessary to simulate a real mouse click.\n\n"
        "<type> is one of:\n"
        " - realmouse\n"
        " - javascript (default)";
    }
    QString command() const {
      switch (_clicktype) {
        case Script::REAL_MOUSE_CLICK: return tag()+" realmouse";
        case Script::JAVASCRIPT_CLICK: default:
          return tag()+" javascript";
      }
    }
    std::shared_ptr<Command> parse(Script* script, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<ClickType> cmd(new ClickType());
      QString choice(script->replacevars(args).trimmed());
      if (choice=="realmouse")
        cmd->_clicktype = Script::REAL_MOUSE_CLICK;
      else if (choice=="javascript")
        cmd->_clicktype = Script::JAVASCRIPT_CLICK;
      else
        throw BadArgument("unknown clicktype: "+choice);
      return cmd;
    }
    bool execute(Script* script, QWebFrame* ) {
      Logger log(this, script);
      script->clicktype(_clicktype);
      return true;
    }
  private:
    Script::ClickType _clicktype;
};

class SetValue: public Command {
  public:
    QString tag() const {
      return "setvalue";
    }
    QString description() const {
      return
        tag()+" <selector> -> '<value>'\n"+
        tag()+" <selector> -> '<value1>', '<value2>', <...>"
        "\n\n"
        "Set the selected element to a given value. It is mostly the same as"
        " the following constuct, except that options in a select are evaluated"
        " correctly:\n\n"
        "  do <selector>\n"
        "    this.value='<value>';\n\n"
        "The second syntax with comma separated list of valus applies for"
        " options in a select element\n\n"
        "If you quote the values, then quote all values with the same"
        " quotes. If you need a comma within a value, you must quote.";
    }
    QString command() const {
      return tag()+" "+_selector+" -> "+_value;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<SetValue> cmd(new SetValue());
      QStringList allargs(args.split("->"));
      if (allargs.size()<2)
        throw BadArgument(tag()+" requires <selector> -> <value>, "
                               "instead of: \""+args+"\"");
      cmd->_selector = allargs.takeFirst().trimmed();
      cmd->_value = allargs.join("->").trimmed();
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      QWebElement element(find(frame, script->replacevars(_selector),
                               script->timeout()-1));
      if (element.isNull())
        error(log, ElementNotFound(script->replacevars(_selector)));
      QString value(script->replacevars(_value));
      if (element.tagName()=="SELECT") {
        // value is a comma seperated list of option values
        QStringList values(commaSeparatedList(value));
        for (QWebElement option: element.findAll("option")) {
          QString name(option.evaluateJavaScript("this.value").toString());
          option.evaluateJavaScript
            ("this.selected="+QString(values.contains(name)?"true;":"false;"));
        }
      } else {
        if (value.size()>1 && value[0]==value[value.size()-1] &&
            (value[0]=='"' || value[0]=='\''))
          element.evaluateJavaScript("this.value="+value+";");
        else
          element.evaluateJavaScript("this.value='"
                                     +value.replace("'", "\\'")
                                     +"';");
      }
      return true;
    }
  private:
    QString _selector;
    QString _value;
};

class Function: public CommandContainer {
  public:
    QString tag() const {
      return "function";
    }
    QString description() const {
      return
        tag()+" <name> [<var1>, <var2>, <...>]\n"
        "  <command1>\n"
        "  <command2>\n"
        "  <...>"
        "\n\n"
        "Define a function with arguments. The arguments are treated like"
        " local variables. In a sequence of scripts within the same testrun,"
        " functions are inherited from all followin scripts, so you can first"
        " load a script file that contains all functions. Within the same file,"
        " a function can be called before the definition.\n\n"
        "If you quote the values, then quote all values with the same"
        " quotes. If you need a comma within a value, you must quote.";
    }
    QString command() const {
      return tag()+" "+_name+" "+_vars.join(", ");
    }
    std::shared_ptr<Command> parse(Script* script, QString args,
                                   QStringList& in, QString file, int line,
                                   int indent) {
      std::shared_ptr<Function> cmd(new Function());
      if (!args.size()) throw BadArgument(tag()+" requires a <name>");
      QStringList allargs(args.split(" "));
      cmd->_name = allargs.takeFirst().trimmed();
      cmd->_vars = commaSeparatedList(allargs.join(' '));
      script->function(cmd->_name, cmd);
      cmd->_script = cmd->subParser(script, subCommandBlock(in), file, line, indent);
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script, false);
      return true;
    }
    bool call(Logger& log, Command* parentCommand, QStringList args,
              Script* script, QWebFrame* frame) {
      try {
        return runScript(log, parentCommand, _script, script, frame, _vars, args);
      } catch (const Exception& x) {
        error(log, FunctionCallFailed(_name, _vars, args, x));
        return false; // never reached due to exception above
      }
    }
  private:
    QString _name;
    QStringList _vars;
};

class Call: public Command {
  public:
    QString tag() const {
      return "call";
    }
    QString description() const {
      return
        tag()+" <name> ['<arg1>', '<arg2>', ...]"
        "\n\n"
        "Calls a function. The number of arguments must be exactly the same"
        " as in the function definition.\n\n"
        "If you quote the values, then quote all values with the same"
        " quotes. If you need a comma within a value, you must quote.";
    }
    QString command() const {
      return tag()+" "+_name+(_args.size()?" '"+_args.join("', '")+"'":"");
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Call> cmd(new Call());
      if (!args.size()) throw BadArgument(tag()+" requires a <name>");
      QStringList allargs(args.split(" "));
      cmd->_name = allargs.takeFirst().trimmed();
      cmd->_args = commaSeparatedList(allargs.join(' '));
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      return script->function(_name)->call(log, this, script->replacevars(_args),
                                                script, frame);
    }
    int steps(Script* parent) {
      return parent->function(_name)->steps(parent);
    }
  public:
    QString _name;
    QStringList _args;
};

class If: public CommandContainer {
  public:
    QString tag() const {
      return "if";
    }
    QString description() const {
      return
        tag()+" <variable> <cmp> <value>\n"
        "  <command1>\n"
        "  <command2>\n"
        "  <...>\n"
        "else\n"
        "  <command3>\n"
        "  <command4>\n"
        "  <...>\n"
        "\n\n"+
        tag()+" <selector> -> <text>\n"
        "  <command1>\n"
        "  <command2>\n"
        "  <...>\n"
        "else\n"
        "  <command3>\n"
        "  <command4>\n"
        "  <...>\n"
        "\n\n"
        "Execute commands conditionally. "
        "The first variant compares a variable to a value. "
        "The comparision <cmp> can be = ! . ^ ~ < >, "
        "which means equal, different, contains, contains not, match, "
        "less (as integer), bigger (as integer). "
        "Match allows a regular expression. "
        "The second variant checks for a text in a selector, "
        "similar to command exists. The text can be empty to just "
        "check for the existence of a selector. "
        "There is an optional else part.";
    }
    QString command() const {
      return tag()+" "+_variable+" "+_cmp+" "+_value
        +(_script.get()?"\n  "+_script->print().join("\n  "):"")
        +(_else.get()?"\nelse\n  "+_else->print().join("\n  "):"");
    }
    std::shared_ptr<Command> parse(Script* script, QString args,
                                   QStringList& in, QString file, int line,
                                   int indent) {
      std::shared_ptr<If> cmd(new If());
      int pos(args.indexOf(QRegularExpression("[=!.^~<>]")));
      int len(1);
      if (args.contains("->")) {
        pos = args.indexOf("->");
        len = 2;
        cmd->_cmp = "->";
      } else {
        if (pos<0) throw BadArgument(tag()+" needs a comparision, not: "+args);
        cmd->_cmp = args[pos];
      }
      cmd->_variable = args.left(pos).trimmed();
      cmd->_value = args.mid(pos+len).trimmed();
      cmd->_script = cmd->subParser(script, subCommandBlock(in), file, line, indent);
      if (in.size() && in.first().contains(QRegularExpression("^ *else *$"))) {
        in.removeFirst();
        cmd->_else = cmd->subParser(script, subCommandBlock(in), file, line, indent);
      }
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script, false);
      QString value(script->replacevars(_value));
      QString selector(script->replacevars(_variable));
      bool check(false);
      if (_cmp=="->") {
        QWebElement firstelement(find(frame, selector,
                                      mrw::max(mrw::min(script->timeout()/3, 10), 2)));
        for (QWebElement element: frame->findAllElements(selector)) {
          if (value.isEmpty() || // just find element
              element.toOuterXml().indexOf(value)!=-1 ||
              element.toPlainText().indexOf(value)!=-1) {
            check = true;
            break;
          }
        }
        log(QString("evaluated expression to ")+(check?"true":"false")+": "
            +selector+" "+_cmp+" "+value);
      } else {
        switch (_cmp[0].toLatin1()) {
          case '=': check = script->variable(_variable)==value;
            break;
          case '!': check = script->variable(_variable)!=value;
            break;
          case '.': check = script->variable(_variable).contains(value);
            break;
          case '^': check = !script->variable(_variable).contains(value);
            break;
          case '~': check =
              script->variable(_variable).contains(QRegularExpression(value));
            break;
          case '<': check = script->variable(_variable).toInt()<value.toInt();
            break;
          case '>': check = script->variable(_variable).toInt()>value.toInt();
            break;
          default:;
        }
        log(QString("evaluated expression to ")+(check?"true":"false")+": "
            +script->variable(_variable)+" "+_cmp+" "+value);
      }
      if (check) return runScript(log, this, _script, script, frame);
      else if (_else) return runScript(log, this, _else, script, frame);
      return true;
    }
  protected:
    int countSteps(Script* parent) {
      int res1(CommandContainer::countSteps(parent)), res2(_else->countSteps()+1);
      return res1 > res2 ? res1 : res2;
    }
  private:
    QString _variable;
    QString _cmp;
    QString _value;
    std::shared_ptr<Script> _else;
};

class While: public CommandContainer {
  public:
    QString tag() const {
      return "while";
    }
    QString description() const {
      return
        tag()+" <variable> <cmp> <value>\n"
        "  <command1>\n"
        "  <command2>\n"
        "  <...>\n"
        "\n\n"+
        tag()+" <selector> -> <text>\n"
        "  <command1>\n"
        "  <command2>\n"
        "  <...>\n"
        "\n\n"
        "Repeats commands conditionally. "
        "The first variant compares a variable to a value. "
        "The comparision <cmp> can be = ! . ^ ~ < >, "
        "which means equal, different, contains, contains not, match, "
        "less (as integer), bigger (as integer). "
        "Match allows a regular expression. "
        "The second variant checks for a text in a selector, "
        "similar to command exists. The text can be empty to just "
        "check for the existence of a selector.";
    }
    QString command() const {
      return tag()+" "+_variable+" "+_cmp+" "+_value
        +(_script.get()?"\n  "+_script->print().join("\n  "):"");
    }
    std::shared_ptr<Command> parse(Script* script, QString args,
                                   QStringList& in, QString file, int line,
                                   int indent) {
      std::shared_ptr<While> cmd(new While());
      int pos(args.indexOf(QRegularExpression("[=!.^~<>]")));
      int len(1);
      if (args.contains("->")) {
        pos = args.indexOf("->");
        len = 2;
        cmd->_cmp = "->";
      } else {
        if (pos<0) throw BadArgument(tag()+" needs a comparision, not: "+args);
        cmd->_cmp = args[pos];
      }
      cmd->_variable = args.left(pos).trimmed();
      cmd->_value = args.mid(pos+len).trimmed();
      cmd->_script = cmd->subParser(script, subCommandBlock(in), file, line, indent);
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script, false);
      QString value(script->replacevars(_value));
      QString selector(script->replacevars(_variable));
      for (bool check(true); check;) {
        if (_cmp=="->") {
          check = false;
          QWebElement firstelement(find(frame, selector,
                                        mrw::max(mrw::min(script->timeout()/3, 10), 2)));
          for (QWebElement element: frame->findAllElements(selector)) {
            if (value.isEmpty() || // just find element
                element.toOuterXml().indexOf(value)!=-1 ||
                element.toPlainText().indexOf(value)!=-1) {
              check = true;
              break;
            }
          }
          log(QString("evaluated expression to ")+(check?"true":"false")+": "
              +selector+" "+_cmp+" "+value);
        } else {
          switch (_cmp[0].toLatin1()) {
            case '=': check = script->variable(_variable)==value;
              break;
            case '!': check = script->variable(_variable)!=value;
              break;
            case '.': check = script->variable(_variable).contains(value);
              break;
            case '^': check = !script->variable(_variable).contains(value);
              break;
            case '~': check =
                script->variable(_variable).contains(QRegularExpression(value));
              break;
            case '<': check = script->variable(_variable).toInt()<value.toInt();
              break;
            case '>': check = script->variable(_variable).toInt()>value.toInt();
              break;
            default:;
          }
          log(QString("evaluated expression to ")+(check?"true":"false")+": "
              +script->variable(_variable)+" "+_cmp+" "+value);
        }
        if (check) if (!runScript(log, this, _script, script, frame)) return false;
      }
      return true;
    }
  private:
    QString _variable;
    QString _cmp;
    QString _value;
};

class TestSuite: public Command {
  public:
    QString tag() const {
      return "testsuite";
    }
    QString description() const {
      return
        tag()+" <name>"
        "\n\n"
        "Start a testsuite and give it a name.";
    }
    QString command() const {
      return tag()+" "+_name;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<TestSuite> cmd(new TestSuite());
      cmd->_name = args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      script->testsuite(script->replacevars(_name));
      return true;
    }
  private:
    QString _name;
};

class TestCase: public Command {
  public:
    QString tag() const {
      return "testcase";
    }
    QString description() const {
      return
        tag()+" <name>"
        "\n\n"
        "Start a testcase and give it a name.";
    }
    QString command() const {
      return tag()+" "+_name;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<TestCase> cmd(new TestCase());
      cmd->_name = args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      script->testclass(script->replacevars(_name));
      return true;
    }
  private:
    QString _name;
};

class Check: public Command {
  public:
    QString tag() const {
      return "check";
    }
    QString description() const {
      return
        tag()+" <value1> <cmp> <value2>\n"+
        tag()+" <value1> <cmp>\n"
        "  <command>"
        "\n\n"
        "Compares two values (you can use variables) or compares a value to the"
        " result of a command. The command should be a command that produces an"
        " output, such as <do>, which returns the result of JavaScript or"
        " <execute>, which returns the output of the executed command, or"
        " <call>, which returns the result of the last command. "
        "The comparision <cmp> can be = ! . ^ ~ < >, "
        "which means equal, different, contains, contains not, match, "
        "less (as integer), bigger (as integer). "
        "Match allows a regular expression. "
        " less than < (integers), larger than > (integers)";
    }
    QString command() const {
      if (_next)
        return tag()+" "+_value1+" "+QString(_cmp)+"\n  "+_next->command();
      else
        return tag()+" "+_value1+" "+QString(_cmp)+" "+_value2;
    }
    std::shared_ptr<Command> parse(Script* script, QString args,
                                   QStringList& in, QString file, int line,
                                   int indent) {
      std::shared_ptr<Check> cmd(new Check());
      cmd->_next = 0;
      QString comp("[=!.^~<>]");
      QStringList allargs = quotedStrings(args, comp, true);
      if (allargs.size()<2 || allargs[1].size()!=1 ||
          !QRegularExpression("^"+comp+"$").match(allargs[1]).hasMatch())
        throw BadArgument(tag()+" needs a comparision, not: "+args);
      if (allargs.size()>3)
        throw BadArgument(tag()+" has at most three arguments");
      cmd->_value1 = allargs[0];
      cmd->_cmp = allargs[1][0].toLatin1();
      if (allargs.size()==3) {
        cmd->_value2 = allargs[2];
      } else {
        if (in.size() && in.first().contains(QRegularExpression("^ "))) {
          cmd->_next = script->parseLine(in, file, line+1, indent+1);
          cmd->_next->log(false); // suppress logging of subcommand
        } else throw BadArgument(tag()+" needs a third argument or a following command");
      }
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      QString value1(script->replacevars(_value1));
      QString value2(script->replacevars(_value2));
      if (_next) {
        _next->execute(script, frame);
        value2 = script->replacevars(_next->result());
      }
      bool check(false);
      switch (_cmp) {
        case '=': check = value1==value2; break;
        case '!': check = value1!=value2; break;
        case '.': check = value1.contains(value2); break;
        case '^': check = !value1.contains(value2); break;
        case '~': check = value1.contains(QRegularExpression(value2)); break;
        case '<': check = value1.toInt()<value2.toInt(); break;
        case '>': check = value1.toInt()>value2.toInt(); break;
        default:;
      }
      log("evaluated expression: "+value1+" "+_cmp+" "+value2);
      if (!check) error(log, CheckFailed(value1, _cmp, value2));
      return true;
    }
  private:
    QString _value1;
    QString _value2;
    char _cmp;
    std::shared_ptr<Command> _next;
};

class For: public CommandContainer {
  public:
    QString tag() const {
      return "for";
    }
    QString description() const {
      return
        tag()+" <variable> -> <val1>, <val2>, <...>\n"
        "  <command1>\n"
        "  <command2>\n"
        "  <...>"
        "\n\n"
        "Executes the given commands with the variable set to the specifier values,"
        "repeated once per given value. The variable is treated like a local variale"
        " in the loop.\n\n"
        "Without values, if there is a global variable with the same name as the"
        " local variable the global variable is parsed as if it were the line after"
        " the dash (->).\n\n"
        "If you quote the values, then quote all values with the same"
        " quotes. If you need a comma within a value, you must quote.";
    }
    QString command() const {
      return tag()+" "+_variable+" "+_vals.join(" ");
    }
    std::shared_ptr<Command> parse(Script* script, QString args,
                                   QStringList& in, QString file, int line, int indent) {
      std::shared_ptr<For> cmd(new For());
      if (!args.size()) throw BadArgument(tag()+" requires a <variable>");
      QStringList allargs(args.split("->"));
      cmd->_variable = allargs.takeFirst().trimmed();
      cmd->_vals = commaSeparatedList(allargs.join("->"));
      cmd->_script = cmd->subParser(script, subCommandBlock(in), file, line, indent);
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      for (QString i: _vals.size()?_vals:commaSeparatedList(script->variable(_variable))) {
        if (!runScript(log, this, _script, script, frame, QStringList()<<_variable, QStringList()<<i))
          return false;
      }
      return true;
    }
    int countSteps(Script* parent) const {
      int sz(1);
      if (_vals.size())
        sz = _vals.size();
      else if (parent->variables().contains(_variable))
        sz = commaSeparatedList(parent->variable(_variable)).size();
      return (sz?sz:1)*CommandContainer::countSteps(parent);
    }
  private:
    QString _variable;
    QStringList _vals;
};

class Echo: public Command {
  public:
    QString tag() const {
      return "echo";
    }
    QString description() const {
      return
        tag()+" <text>"
        "\n\n"
        "Echoes a text to the log.";
    }
    QString command() const {
      return tag();
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Echo> cmd(new (Echo));
      cmd->_text = args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      log(script->replacevars(_text));
      return true;
    }
  private:
    QString _text;
};

class OfflineStoragePath: public Command {
  public:
    QString tag() const {
      return "offline-storage-path";
    }
    QString description() const {
      return
        tag()+" <path>"
        "\n\n"
        "Set offline storage path. Defaults to /tmp.";
    }
    QString command() const {
      return tag()+" "+_path;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<OfflineStoragePath> cmd(new OfflineStoragePath());
      cmd->_path = args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      QDir path(_path);
      if (!path.exists() && !path.mkpath(_path))
        error(log, DirectoryCannotBeCreated(_path));
      TestWebPage* page(dynamic_cast<TestWebPage*>(frame->page()));
      page->settings()->setOfflineStoragePath(_path);
      return true;
    }
  private:
    QString _path;
};

class ClearCookies: public Command {
  public:
    QString tag() const {
      return "clear-cookies";
    }
    QString description() const {
      return
        tag()+" <url>"
        "\n\n"
        "Clear all cookies of given URL <url>, or of the current URL if no"
        " <url> is specified.";
    }
    QString command() const {
      return tag()+(_url.isEmpty()?"":" "+_url);
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<ClearCookies> cmd(new ClearCookies());
      if (args.size()) cmd->_url = args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      QString url(_url);
      if (url.isEmpty()) url = frame->url().toString();
      QNetworkCookieJar* cookies = frame->page()->networkAccessManager()->cookieJar();
      for (QNetworkCookie cookie: cookies->cookiesForUrl(url)) {
        log("delete cookie "+cookie.name());
        cookies->deleteCookie(cookie);
      }
      return true;
    }
  private:
    QString _url;
};

class Include: public CommandContainer {
  public:
    QString tag() const {
      return "include";
    }
    QString description() const {
      return
        tag()+" <filename>"
        "\n\n"
        "Include a test script.";
    }
    QString command() const {
      return tag()+" "+_filename;
    }
    std::shared_ptr<Command> parse(Script* script, QString args,
                                   QStringList&, QString, int, int indent) {
      std::shared_ptr<Include> cmd(new Include());
      cmd->_filename = args;
      QFile f(cmd->_filename);
      if (!f.open(QIODevice::ReadOnly)) throw OpenIncludeFailed(cmd->_filename);
      QString txt(QString::fromUtf8(f.readAll()));
      try {
        cmd->_script = cmd->subParser(script, txt.split('\n'), cmd->_filename, 0, indent);
      } catch (Exception& e) {
        throw ParseIncludeFailed(cmd->_filename, e.what());
      }
      for (QString key: cmd->_script->functions()) // copy new functions to parent
        script->function(key, cmd->_script->function(key));
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      try {
        return runScript(log, this, _script, script, frame);
      } catch (Exception& e) {
        error(log, ExecuteIncludeFailed(_filename, e.what()));
        return false; // never reached due to exception above
      }
    }
  private:
    QString _filename;
};

class Case: public Command {
  public:
    QString tag() const {
      return "case";
    }
    QString description() const {
      return
        tag()+" <variable>\n"
        "  <cmp1> <value1>\n"
        "    <command>\n"
        "    <command>\n"
        "  <cmp2> <value2>\n"
        "    <command>\n"
        "    <command>\n"
        "  <...>\n"
        "  default\n"
        "    <command>\n"
        "    <command>\n"
        "    <...>\n"
        "\n\n"+
        tag()+" <selector>\n"
        "  -> <text1>\n"
        "    <command>\n"
        "    <command>\n"
        "  -> <text2>\n"
        "    <command>\n"
        "    <command>\n"
        "  <...>\n"
        "  default\n"
        "    <command>\n"
        "    <command>\n"
        "    <...>\n"
        "\n\n"
        "Execute commands conditionally depending on a variable. "
        "It is equivalent to neested if-else-if commands."
        "The first variant compares a variable to a value. "
        "The comparision <cmp> can be = ! . ^ ~ < >, "
        "which means equal, different, contains, contains not, match, "
        "less (as integer), bigger (as integer). "
        "Match allows a regular expression. "
        "The second variant checks for a text in a selector, "
        "similar to command exists. "
        "There is an optional default part that applies if none "
        "of the previous conditions match.";
    }
    QString command() const {
      QString body;
      for (Condition condition: _conditions) {
        body += "\n  "+condition.cmp+" "+condition.value+"\n    "
          +condition.script->print().join("\n    ");
      }
      return tag()+" "+_variable+body;
    }
    std::shared_ptr<Command> parse(Script* script, QString args,
                                   QStringList& in, QString file, int line,
                                   int indent) {
      std::shared_ptr<Case> cmd(new Case());
      if (!args.size()) throw BadArgument(tag()+" requires a <variable> or <selector>");
      cmd->_variable = args;
      QStringList body(subCommandBlock(in));
      if (!body.size()) throw BadArgument(tag()+" requires a body");
      while (body.size()) {
        ++line;
        QStringList parts(body.takeFirst().split(' '));
        QString cmp(parts.takeFirst());
        QString value(parts.join(' '));
        if (!cmp.contains(QRegularExpression("^[=!.^~<>]|->|default$")))
          throw BadArgument(tag()+" needs a comparision, not: "+cmp);
        std::shared_ptr<Script> sub
          (cmd->subParser(script, subCommandBlock(body), file, line, indent+1));
        cmd->_conditions.append(Condition(cmp, value, sub));
      }
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script, false);
      QString selector(script->replacevars(_variable));
      for (Condition condition: _conditions) {
        QString value(script->replacevars(condition.value));
        bool check(false);
        if (condition.cmp=="default") {
          log("terminate with default branch");
          check = true;
        } else if (condition.cmp=="->") {
          QWebElement firstelement(find(frame, selector,
                                        mrw::max(mrw::min(script->timeout()/3, 10), 2)));
          for (QWebElement element: frame->findAllElements(selector)) {
            if (value.isEmpty() || // just find element
                element.toOuterXml().indexOf(value)!=-1 ||
                element.toPlainText().indexOf(value)!=-1) {
              check = true;
              break;
            }
          }
          log(QString("evaluated expression to ")+(check?"true":"false")+": "
              +selector+" "+condition.cmp+" "+value);
        } else {
          switch (condition.cmp[0].toLatin1()) {
            case '=': check = script->variable(_variable)==value;
              break;
            case '!': check = script->variable(_variable)!=value;
              break;
            case '.': check = script->variable(_variable).contains(value);
              break;
            case '^': check = !script->variable(_variable).contains(value);
              break;
            case '~': check =
                script->variable(_variable).contains(QRegularExpression(value));
              break;
            case '<': check = script->variable(_variable).toInt()<value.toInt();
              break;
            case '>': check = script->variable(_variable).toInt()>value.toInt();
              break;
            default:;
          }
          log(QString("evaluated expression to ")+(check?"true":"false")+": "
              +script->variable(_variable)+" "+condition.cmp+" "+value);
        }
        if (check) return runScript(log, this, condition.script, script, frame);
      }
      return true;
    }
  private:
    int countSteps(Script*) {
      int res1(0);
      for (auto condition: _conditions) {
        int res2(condition.script->countSteps()+1);
        if (res2>res1) res1=res2;
      }
      return res1;
    }
    struct Condition {
        Condition(QString c, QString v, std::shared_ptr<Script> s):
            cmp(c), value(v), script(s) {
        }
        Condition() {}
        QString cmp;
        QString value;
        std::shared_ptr<Script> script;
    };
    QString _variable;
    QVector<Condition> _conditions;
};

class Fail: public Command {
  public:
    QString tag() const {
      return "fail";
    }
    QString description() const {
      return
        tag()+" <text>"
        "\n\n"
        "Fail with error text.";
    }
    QString command() const {
      return tag()+" "+_text;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Fail> cmd(new Fail());
      cmd->_text = args;
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      error(log, TestFailed(script->replacevars(_text)));
      return true; // dummy
    }
  private:
    QString _text;
};

class Auth: public Command {
  public:
    QString tag() const {
      return "auth";
    }
    QString description() const {
      return
        tag()+" <realm> <username> <password>"
        "\n\n"+
        tag()+" <realm>"
        "\n\n"
        "Set basic authentication credentials for <realm> to"
        " <username> and <password>. If no realm is given,"
        " the credentials for the given realm are removed.";
    }
    QString command() const {
      return tag()+" "+_username+" "+_password;
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Auth> cmd(new Auth());
      QStringList allargs = args.split(" ");
      if (!allargs.size()) throw BadArgument("requires at least a <realm>");
      cmd->_realm = allargs.takeFirst();
      if (allargs.size() && allargs.size()==2) {
        cmd->_username=allargs[0];
        cmd->_password=allargs[1];
      } else {
        throw BadArgument(QString("requires <username> and <password>, but %1 was given")
                          .arg(args));
      }
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      script->auth(_realm, _username, _password);
      return true;
    }
  private:
    QString _realm;
    QString _username;
    QString _password;
};

class Ignore: public Command {
  public:
    QString tag() const {
      return "ignore";
    }
    QString description() const {
      return
        tag()+" <signal1> <signal2> <...>"
        "\n\n"
        "Ignores a specific signal. It will not be placed in the queue "
        "and any expect for this signal is always true. You can call "
        "ignore with a space separated list of signal names. You cannot "
        "specify signal arguments. An empty ignore ignores all signals.";
    }
    QString command() const {
      return tag()+" "+_signals.join(" ");
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<Ignore> cmd(new Ignore());
      cmd->_signals = args.split(' ', QString::SkipEmptyParts);
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      script->ignore(_signals);
      return true;
    }
  private:
    QStringList _signals;
};

class UnIgnore: public Command {
  public:
    QString tag() const {
      return "unignore";
    }
    QString description() const {
      return
        tag()+" <signal1> <signal2> <...>"
        "\n\n"
        "Undo ignoring a specific signal. It will be placed in the queue "
        "and any expect this signal checks the queue. You can call "
        "unignore with a space separated list of signal names. You cannot "
        "specify signal arguments. An empty ignore activates all signals.";
    }
    QString command() const {
      return tag()+" "+_signals.join(" ");
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<UnIgnore> cmd(new UnIgnore());
      cmd->_signals = args.split(' ', QString::SkipEmptyParts);
      return cmd;
    }
    bool execute(Script* script, QWebFrame*) {
      Logger log(this, script);
      script->unignore(_signals);
      return true;
    }
  private:
    QStringList _signals;
};

/* Template:
class : public Command {
  public:
    QString tag() const {
      return "";
    }
    QString description() const {
      return
        tag()+
        "\n\n"
        "";
    }
    QString command() const {
      return tag();
    }
    std::shared_ptr<Command> parse(Script*, QString args,
                                   QStringList&, QString, int, int) {
      std::shared_ptr<> cmd(new ());
      return cmd;
    }
    bool execute(Script* script, QWebFrame* frame) {
      Logger log(this, script);
      return true;
    }
};
*/

inline bool Screenshot::execute(Script* script, QWebFrame* frame) {
  Logger log(this, script);
  if (!script->screenshots()) {
    log("screenshots disabled");
    return true;
  }
  QString filename(screenshot(log, line(), script->targetdir(),
                              script->testclass(),
                              script->replacevars(_filename), frame));
  log["[[ATTACHMENT|"+filename+"]]"];
  return true;
}

inline Logger::Logger(Command* command, Script* script, bool showLines):
    _command(command), _script(script) {
  if (command) {
    if (_command->log()) {
      if (showLines)
        _script->log("\\ "+_command->command(), _command);
      else
        _script->log("\\ "+_command->command().split('\n').first(), _command);
    }
  }
}
inline void Logger::operator()(QString txt) {
  if (!_command || _command->log()) _script->log("  "+txt, _command);
}
inline void Logger::operator[](QString txt) {
  _script->plainlog(txt);
}
inline Logger::~Logger() {
  if (_command) {
    if (_command->log()) _script->log("/ "+_command->tag(), _command);
  }
}

inline void Command::realMouseClick(Logger& log, QWebFrame* frame,
                                    Script* script, QString selector) {
  QWebElement element(find(frame, selector, script->timeout()-1));
  if (element.isNull()) error(log, ElementNotFound(selector));
  realMouseClick(element);
}

inline std::shared_ptr<Script> Command::subParser(Script* parent, const QStringList& in,
                                                  const QString& file,
                                                  int line, int indent) {
  std::shared_ptr<Script> res(new Script);
  for (QString key: parent->functions()) // copy functions from parent
    res->function(key, parent->function(key));
  res->parse(in, file, line+1, indent+1);
  return res;
}

inline bool Command::runScript(Logger& log, Command* parentCommand,
                               std::shared_ptr<Script> script,
                               Script* parent, QWebFrame* frame,
                               QStringList vars,
                               QStringList args) {
  Script scriptCopy(*script); // only work with a copy of script
  scriptCopy.set(*parent);
  scriptCopy.parent(parent);
  if (args.size()!=vars.size())
    error(log, WrongNumberOfArguments(vars, args));
  for (QStringList::iterator var(vars.begin()), arg(args.begin());
       var<vars.end() && arg<args.end(); ++var, ++arg) {
    parent->log("argument: "+*var+" = "+parent->replacevars(*arg),
                parentCommand);
    scriptCopy.set(*var, parent->replacevars(*arg));
  }
  try {
    assert(connect(&scriptCopy, SIGNAL(logging(QString)),
                   parent, SLOT(parentlog(QString))));
    assert(connect(&scriptCopy, SIGNAL(progress(QString, int, int, int)),
                   parent, SLOT(innerProgress(QString, int, int))));
    parent->removeSignals(frame);
    bool res(scriptCopy.run(frame));
    parent->addSignals(frame);
    disconnect(&scriptCopy, SIGNAL(progress(QString, int, int, int)),
               parent, SLOT(innerProgress(QString, int, int)));
    disconnect(&scriptCopy, SIGNAL(logging(QString)),
               parent, SLOT(parentlog(QString)));
    parentCommand->_result = scriptCopy.result();
    for (QString key: scriptCopy.variables()) // copy new variables to parent
      if (!vars.contains(key)) parent->set(key, scriptCopy.variable(key));
    parent->ignore(scriptCopy); // copy ignore list
    if (parentCommand->_result.size())
      parent->log("result: "+parentCommand->_result);
    return res;
  } catch (const Exception& x) {
    parent->addSignals(frame);
    disconnect(&scriptCopy, SIGNAL(progress(QString, int, int, int)),
               parent, SLOT(innerProgress(QString, int, int)));
    disconnect(&scriptCopy, SIGNAL(logging(QString)),
               parent, SLOT(parentlog(QString)));
    throw;
  }
}

inline void Script::initPrototypes() {
  add(new Do);
  add(new Load);
  add(new Expect);
  add(new Screenshot);
  add(new Open);
  add(new Sleep);
  add(new Exit);
  add(new IgnoreTo);
  add(new Label);
  add(new Upload);
  add(new Exists);
  add(new Not);
  add(new Execute);
  add(new Download);
  add(new Click);
  add(new Set);
  add(new UnSet);
  add(new Timeout);
  add(new CaCertificate);
  add(new ClientCertificate);
  add(new ::ClickType);
  add(new SetValue);
  add(new Function);
  add(new Call);
  add(new If);
  add(new While);
  add(new TestSuite);
  add(new TestCase);
  add(new Check);
  add(new For);
  add(new Echo);
  add(new OfflineStoragePath);
  add(new ClearCookies);
  add(new Include);
  add(new Case);
  add(new Fail);
  add(new Auth);
  add(new Ignore);
  add(new UnIgnore);
}

#endif
