// ============================================================ //
//                                                              //
//   File      : TreeRead.cxx                                   //
//   Purpose   : load tree from file                            //
//                                                              //
//   Institute of Microbiology (Technical University Munich)    //
//   www.arb-home.de                                            //
//                                                              //
// ============================================================ //

#include "TreeRead.h"
#include <TreeNode.h>

#include <arb_msg_fwd.h>
#include <arb_strbuf.h>
#include <arb_strarray.h>
#include <arb_file.h>
#include <arb_defs.h>
#include <algorithm>

#define tree_assert(cond) arb_assert(cond)

/*!******************************************************************************************
                    load a tree from file system
********************************************************************************************/

// --------------------
//      TreeReader

class TreeReader : virtual Noncopyable {
    enum tr_lfmode { LF_UNKNOWN, LF_N, LF_R, LF_NR, LF_RN, };

    int   unnamed_counter;
    char *tree_file_name;
    FILE *in;
    int   last_character;          // may be EOF
    int   line_cnt;

    GBS_strstruct tree_comment;
    GBT_LEN       max_found_branchlen;
    double        max_found_bootstrap;
    tr_lfmode     lfmode;

    char *warnings;

    TreeRoot *troot;

#define MAX_READER_WARN 100 // maximum warnings show for each kind of separately counted warnings

    struct Count {
        int dropped_leaf_groups;
        int dropped_leaf_remarks;
        int dropped_redefined_groups;
        int dropped_redefined_remarks;
        Count() : dropped_leaf_groups(0), dropped_leaf_remarks(0), dropped_redefined_groups(0), dropped_redefined_remarks(0) {}

        bool warnings_suppressed() const { return
                dropped_leaf_groups>MAX_READER_WARN  || dropped_redefined_groups>MAX_READER_WARN ||
                dropped_leaf_remarks>MAX_READER_WARN || dropped_redefined_remarks>MAX_READER_WARN;
        }
    } count;

    void setError(const char *message);
    void setErrorAt(const char *message);
    void setExpectedError(const char *expected);

    int get_char();
    int read_char();
    int read_tree_char(); // extracts comments and ignores whitespace outside comments

    char *content_ahead(size_t how_many, bool show_eof);

    void drop_tree_char(char expected);

    void setBranchName_acceptingBootstrap(TreeNode *node, char*& name);

    // The eat-functions below assume that the "current" character
    // has already been read into 'last_character':
    void  eat_white();
    __ATTR__USERESULT bool eat_number(GBT_LEN& result);
    char *eat_quoted_string();
    bool  eat_and_set_name_and_length(TreeNode *node, GBT_LEN& len);

    char *unnamedNodeName() { return GBS_global_string_copy("unnamed%i", ++unnamed_counter); }

    TreeNode *load_subtree(GBT_LEN& nodeLen);
    TreeNode *load_named_node(GBT_LEN& nodeLen);

public:

    TreeReader(FILE *input, const char *file_name, TreeRoot *troot_);
    ~TreeReader();

    TreeNode *load() {
        GBT_LEN   rootNodeLen = DEFAULT_BRANCH_LENGTH_MARKER; // ignored dummy
        TreeNode *tree        = load_named_node(rootNodeLen);

        if (!error) {
            if (rootNodeLen != DEFAULT_BRANCH_LENGTH_MARKER && rootNodeLen != 0.0) {
                add_warning("Length specified for root-node has been ignored");
            }
            if (!tree->is_leaf()) { // handle special cases: tree "();" and "(a);"(?)
                const char *remark = tree->get_remark(); // remark of root node
                if (remark) {
                    add_warningf("Remark specified for root-node ('%s') has been ignored", remark);
                    tree->remove_remark();
                }
            }

            // check for unexpected input
            if (last_character == ';') read_tree_char(); // accepts ';'
            if (last_character != EOF) {
                char *unused_input = content_ahead(30, false);
                add_warningf("Unexpected input-data after tree: '%s'", unused_input);
                free(unused_input);
            }

            tree->announce_tree_constructed();
        }
        return tree;
    }

    GB_ERROR error;

    void add_warning(const char *msg) {
        if (warnings) freeset(warnings, GBS_global_string_copy("%s\n%s", warnings, msg));
        else warnings = GBS_global_string_copy("Warning(s): %s", msg);
    }
    __ATTR__FORMAT(2) void add_warningf(const char *format, ...) { FORWARD_FORMATTED(add_warning, format); }

    void add_drop_summary_warning_if_suppressed();
    GB_ERROR get_warnings() const { return warnings; } // valid until TreeReader is destroyed

    char *takeComment() {
        // can only be called once (further calls will return NULp)
        return tree_comment.release();
    }

    double get_max_found_bootstrap() const { return max_found_bootstrap; }
    GBT_LEN get_max_found_branchlen() const { return max_found_branchlen; }
};

TreeReader::TreeReader(FILE *input, const char *file_name, TreeRoot *troot_)
    : unnamed_counter(0),
      tree_file_name(strdup(file_name)),
      in(input),
      last_character(0),
      line_cnt(1),
      tree_comment(2048),
      max_found_branchlen(-1),
      max_found_bootstrap(-1),
      lfmode(LF_UNKNOWN),
      warnings(NULp),
      troot(troot_),
      error(NULp)
{
    read_tree_char();
}

TreeReader::~TreeReader() {
    free(warnings);
    free(tree_file_name);
}

void TreeReader::setError(const char *message) {
    tree_assert(!error);
    error = GBS_global_string("Error reading %s:%i: %s",
                              tree_file_name, line_cnt, message);
}
char *TreeReader::content_ahead(size_t how_many, bool show_eof) {
    char show[how_many+1+4]; // 4 = oversize of '<EOF>'
    size_t i;
    for (i = 0; i<how_many; ++i) {
        show[i] = last_character;
        if (show[i] == EOF) {
            if (show_eof) {
                strcpy(show+i, "<EOF>");
                i += 5;
            }
            break;
        }
        read_char();
    }
    show[i] = 0;
    return strdup(show);
}

void TreeReader::setErrorAt(const char *message) {
    if (last_character == EOF) {
        setError(GBS_global_string("%s while end-of-file was reached", message));
    }
    else {
        char *show = content_ahead(30, true);
        setError(GBS_global_string("%s while looking at '%s'", message, show));
        free(show);
    }
}

void TreeReader::setExpectedError(const char *expected) {
    setErrorAt(GBS_global_string("Expected %s", expected));
}

int TreeReader::get_char() {
    // reads character from stream
    // - converts linefeeds for DOS- and MAC-textfiles
    // - increments line_cnt

    int c   = getc(in);
    int inc = 0;

    if (c == '\n') {
        switch (lfmode) {
            case LF_UNKNOWN: lfmode = LF_N; inc = 1; break;
            case LF_N:       inc = 1; break;
            case LF_R:       lfmode = LF_RN; c = get_char(); break;
            case LF_NR:      c = get_char(); break;
            case LF_RN:      inc = 1; break;
        }
    }
    else if (c == '\r') {
        switch (lfmode) {
            case LF_UNKNOWN: lfmode = LF_R; inc = 1; break;
            case LF_R:       inc = 1; break;
            case LF_N:       lfmode = LF_NR; c = get_char(); break;
            case LF_RN:      c = get_char(); break;
            case LF_NR:      inc = 1; break;
        }
        if (c == '\r') c = '\n';     // never report '\r'
    }
    if (inc) line_cnt++;

    return c;
}

int TreeReader::read_tree_char() {
    // reads over tree comment(s) and whitespace.
    // tree comments are stored inside TreeReader

    bool done = false;
    int  c    = ' ';

    while (!done && !error) {
        c = get_char();
        if (c == ' ' || c == '\t' || c == '\n') ; // skip
        else if (c == '[') {    // collect tree comment(s)
            int openBrackets = 1;
            if (tree_comment.get_position()) {
                tree_comment.put('\n'); // not first comment -> add new line
            }

            while (openBrackets && !error) {
                c = get_char();
                switch (c) {
                    case EOF:
                        setError("Reached end of file while reading comment");
                        break;
                    case ']':
                        openBrackets--;
                        if (openBrackets) tree_comment.put(c); // write all but last closing brackets
                        break;
                    case '[':
                        openBrackets++;
                        // fall-through
                    default:
                        tree_comment.put(c);
                        break;
                }
            }
        }
        else done = true;
    }

    last_character = c;
    return c;
}

int TreeReader::read_char() {
    int c = get_char();
    last_character = c;
    return c;
}

void TreeReader::eat_white() {
    int c = last_character;
    while ((c == ' ') || (c == '\n') || (c == '\r') || (c == '\t')) {
        c = read_char();
    }
}

bool TreeReader::eat_number(GBT_LEN& result) {
    char    strng[256];
    char   *s = strng;
    int     c = last_character;

    while (((c<='9') && (c>='0')) || (c=='.') || (c=='-') ||  (c=='+') || (c=='e') || (c=='E')) {
        *(s++) = c;
        c = read_char();
    }
    *s = 0;

    GB_ERROR failed_to_convert = GB_safe_atof(strng, &result);
    if (failed_to_convert) {
        setErrorAt(failed_to_convert);
        return false;
    }

    eat_white();

    bool consumed_some_length = strng[0];
    return consumed_some_length;
}

char *TreeReader::eat_quoted_string() {
    /*! Read in a quoted or unquoted string.
     * in quoted strings double quotes ('') are replaced by (').
     *
     * @return
     *     NULp in case of error,
     *     "" if no string present,
     *     otherwise the found string.
     */

    const int MAX_NAME_LEN = 1000;

    char  buffer[MAX_NAME_LEN+2];
    char *s = buffer;
    int   c = last_character;

#define NAME_TOO_LONG ((s-buffer)>MAX_NAME_LEN)

    if (c == '\'' || c == '"') {
        char found_quote = c;

        c = read_char();
        while (c!=EOF && c!=found_quote) {
            *(s++) = c;
            if (NAME_TOO_LONG) { c = 0; break; }
            c = read_char();
        }
        if (c == found_quote) c = read_tree_char();
    }
    else {
#if 0
        // previous behavior: skip prefixes matching PRE '_* *'
        // (reason unknown; behavior exists since [2])
        // conflicts with replacement of problematic character done in ../TREE_WRITE/TreeWrite.cxx@replace_by_underscore
        // -> disabled
        while (c == '_') c = read_tree_char();
        while (c == ' ') c = read_tree_char();
#endif
        while (c!=':' && c!=EOF && c!=',' && c!=';' && c != ')') {
            *(s++) = c;
            if (NAME_TOO_LONG) break;
            c = read_tree_char();
        }
    }
    *s = 0;
    if (NAME_TOO_LONG) {
        setError(GBS_global_string("Name '%s' is longer than %i bytes", buffer, MAX_NAME_LEN));
        return NULp;
    }
    return strdup(buffer);
}

inline void append_leaf_redef_details(GBS_strstruct& summary, int droppedAtLeaf, int droppedRedefined) {
    if (droppedAtLeaf) {
        summary.putlong(droppedAtLeaf);
        summary.cat(" at leafs");
    }
    if (droppedAtLeaf && droppedRedefined) {
        summary.cat(" + ");
    }
    if (droppedRedefined) {
        summary.putlong(droppedRedefined);
        summary.cat(" redefined");
    }
}

void TreeReader::add_drop_summary_warning_if_suppressed() {
    if (count.warnings_suppressed()) {
        GBS_strstruct summary(200);
        summary.cat("Drop summary: ");

        bool dropped_remarks = count.dropped_leaf_remarks || count.dropped_redefined_remarks;
        bool dropped_groups  = count.dropped_leaf_groups || count.dropped_redefined_groups;

        if (dropped_groups) {
            summary.cat("groups: ");
            append_leaf_redef_details(summary, count.dropped_leaf_groups, count.dropped_redefined_groups);
        }
        if (dropped_groups && dropped_remarks) {
            summary.cat(", ");
        }
        if (dropped_remarks) {
            summary.cat("support-values/remarks: ");
            append_leaf_redef_details(summary, count.dropped_leaf_remarks, count.dropped_redefined_remarks);
        }
        summary.put('.');

        add_warning(summary.get_data());
    }
}

void TreeReader::setBranchName_acceptingBootstrap(TreeNode *node, char*& name) {
    // store groupname and/or bootstrap value.
    //
    // ARBs extended newick format allows 4 kinds of node-names:
    //     'groupname'
    //     'bootstrap'
    //     'bootstrap:groupname' (needs to be quoted)
    //     'remark:groupname' (needs to be quoted)
    //
    // where
    //     'bootstrap' is sth interpretable as double (optionally followed by '%')
    //     'groupname' is sth not interpretable as double
    //
    // If a groupname           is detected, it is stored in node->name
    // If a bootstrap or remark is detected, it is stored in node->remark_branch
    //
    // Bootstrap values will be scaled up by factor 100.
    // Wrong scale-ups (to 10000) will be corrected by calling TREE_scale() after the whole tree has been loaded.
    //
    // Note:  this method may be called with leafs, if the input data contains "(leafname)groupname" or similar.
    //        In this case it gets called for 'groupname' (not for 'leafname').

    char *group = NULp;
    {
        double      bootstrap;
        const char *group_part   = name;
        char       *remark       = NULp;
        bool        is_bootstrap = parse_treelabel(group_part, bootstrap, remark);

        if (is_bootstrap || remark) {
            tree_assert(contradicted(is_bootstrap, remark));

            if (node->is_leaf()) {
                if (count.dropped_leaf_remarks<MAX_READER_WARN) {
                    if (is_bootstrap) {
                        add_warningf("Dropped support-value ('%g') specified for a single-node-subtree", bootstrap);
                    }
                    else {
                        add_warningf("Dropped remark ('%s') specified for a single-node-subtree", remark);
                    }

                    if (++count.dropped_leaf_remarks == MAX_READER_WARN) {
                        add_warning("[Note: further warnings of this type will be suppressed]");
                    }
                }
            }
            else {
                const char *existingRemark = node->get_remark();
                if (existingRemark) {
                    if (count.dropped_redefined_remarks<MAX_READER_WARN) {
                        if (is_bootstrap) {
                            add_warningf("Dropped support-value redefinition '%g' (edge already labeled as '%s')", bootstrap, existingRemark);
                        }
                        else {
                            add_warningf("Dropped remark redefinition '%s' (edge already labeled as '%s')", remark, existingRemark);
                        }
                        if (++count.dropped_redefined_remarks == MAX_READER_WARN) {
                            add_warning("[Note: further warnings of this type will be suppressed]");
                        }
                        freenull(remark);
                    }
                }
                else {
                    if (is_bootstrap) {
                        bootstrap = bootstrap*100.0; // needed if bootstrap values are between 0.0 and 1.0 (downscaling is done later)
                        if (bootstrap > max_found_bootstrap) { max_found_bootstrap = bootstrap; }
                        node->set_bootstrap(bootstrap);
                    }
                    else {
                        node->set_remark(remark);
                    }
                }
            }
        }

        if (group_part) {
            if (group_part == name) reassign(group, name); // use whole input as groupname
            else                    group = strdup(group_part);
        }

        freenull(name);
        freenull(remark);
    }

    if (group) {
        if (!group[0]) { // empty group name (e.g. "3%:" or "remark:")
            freenull(group);
        }
        else if (node->name) {
            if (node->is_leaf()) {
                if (count.dropped_leaf_groups<MAX_READER_WARN) {
                    add_warningf("Dropped group name specified for a single-node-subtree ('%s')", group);
                    if (++count.dropped_leaf_groups == MAX_READER_WARN) {
                        add_warning("[Note: further warnings of this type will be suppressed]");
                    }
                }
                freenull(group);
            }
            else {
                if (count.dropped_redefined_groups<MAX_READER_WARN) {
                    add_warningf("Dropped group name redefinition '%s' (furcation already named '%s')",
                                 group, node->name);
                    if (++count.dropped_redefined_groups == MAX_READER_WARN) {
                        add_warning("[Note: further warnings of this type will be suppressed]");
                    }
                }
                freenull(group);
            }
        }
        else {
            reassign(node->name, group);
        }

        tree_assert(implicated(node->name, node->name[0]));

        if (node->name) {
            GB_ERROR badNameError = GBT_check_valid_group_name(node->name);
            if (badNameError) setErrorAt(badNameError);
        }
    }
}

void TreeReader::drop_tree_char(char expected) {
    if (last_character != expected) {
        setExpectedError(GBS_global_string("'%c'", expected));
    }
    read_tree_char();
}

bool TreeReader::eat_and_set_name_and_length(TreeNode *node, GBT_LEN& nodeLen) {
    // reads optional branch-name and -length
    //
    // if 'nodeLen' contains DEFAULT_BRANCH_LENGTH_MARKER, it gets overwritten with any found length-specification
    // otherwise found length is added to 'nodeLen'
    //
    // sets the branch-name of 'node', if a name is found (e.g. sth like "(...)'name':0.5")
    //
    // returns true if successful, false otherwise (TreeReader::error is set then)

    bool done            = false;
    bool length_consumed = false;

    while (!done && !error) {
        switch (last_character) {
            case ';':
            case ',':
            case ')':
                done = true;
                break;
            case ':':
                if (!error && length_consumed) setErrorAt("Unexpected ':' (already read a branchlength)");
                if (!error) drop_tree_char(':');
                if (!error) {
                    GBT_LEN foundlen;
                    if (eat_number(foundlen)) {
                        if (is_marked_as_default_len(nodeLen)) {
                            nodeLen = foundlen;
                        }
                        else {
                            tree_assert(node->is_leaf()); // should only happen when a single leaf in parenthesis was read
                            nodeLen += foundlen;        // sum leaf and node lengths
                        }
                        max_found_branchlen = std::max(max_found_branchlen, nodeLen);
                    }
                    else {
                        // accept (don't superseede) errors set by eat_number()
                        if (!error) setExpectedError("valid length");
                    }
                }
                length_consumed = true;
                break;

            case EOF:
                done = true;
                break;

            default: {
                char *branchName = eat_quoted_string();
                tree_assert(contradicted(branchName, error));
                if (branchName) {
                    if (branchName[0]) setBranchName_acceptingBootstrap(node, branchName);
                    else               freenull(branchName);
                }
                break;
            }
        }
    }

    return !error;
}

static TreeNode *createLinkedTreeNode(const TreeRoot& nodeMaker, TreeNode *left, GBT_LEN leftlen, TreeNode *right, GBT_LEN rightlen) { // @@@ move into class GBT_tree (as ctor) - or better move into TreeNodeFactory
    TreeNode *node = nodeMaker.makeNode();

    node->leftson  = left;
    node->leftlen  = leftlen;
    node->rightson = right;
    node->rightlen = rightlen;

    left->father  = node;
    right->father = node;

    return node;
}

TreeNode *TreeReader::load_named_node(GBT_LEN& nodeLen) {
    // reads a node or subtree.
    // a single node is expected to have a name (or will be auto-named)
    // subtrees may have a name (groupname)
    TreeNode *node = NULp;

    if (last_character == '(') {
        node = load_subtree(nodeLen);
    }
    else {                      // single node
        eat_white();
        char *name = eat_quoted_string();
        tree_assert(contradicted(name, error));
        if (name) {
            if (!name[0]) freeset(name, unnamedNodeName());

            node       = troot->makeNode();
            node->name = name;
            node->markAsLeaf();
        }
    }
    if (node && !error) {
        if (!eat_and_set_name_and_length(node, nodeLen)) {
            node->forget_origin();
            destroy(node, troot);
            node = NULp;
        }
    }
    tree_assert(contradicted(node, error));
    tree_assert(!node || !node->is_leaf() || node->name); // leafs need to be named here
    return node;
}


TreeNode *TreeReader::load_subtree(GBT_LEN& nodeLen) {
    // loads a subtree (i.e. expects parenthesis around one or several nodes)
    //
    // 'nodeLen' normally is set to DEFAULT_BRANCH_LENGTH_MARKER
    //           or to length of single node (if parenthesis contain only one node)
    //
    // length and/or name behind '(...)' are not parsed (has to be done by caller).
    //
    // if subtree contains a single node (or a single other subtree), 'name'+'remark_branch' are
    // already set, when load_subtree() returns - otherwise they are NULp.

    TreeNode *node = NULp;

    drop_tree_char('(');

    GBT_LEN   leftLen = DEFAULT_BRANCH_LENGTH_MARKER;
    TreeNode *left    = load_named_node(leftLen);

    if (left) {
        switch (last_character) {
            case ')':               // single node
                nodeLen = leftLen;
                node    = left;
                left    = NULp;
                break;

            case ',': {
                GBT_LEN   rightLen = DEFAULT_BRANCH_LENGTH_MARKER;
                TreeNode *right    = NULp;

                while (last_character == ',' && !error) {
                    if (right) { // multi-branch
                        TreeNode *pair = createLinkedTreeNode(*troot, left, leftLen, right, rightLen);

                        left  = pair; leftLen = 0;
                        right = NULp; rightLen = DEFAULT_BRANCH_LENGTH_MARKER;
                    }

                    drop_tree_char(',');
                    if (!error) {
                        right = load_named_node(rightLen);
                    }
                }

                if (!error) {
                    if (last_character == ')') {
                        node    = createLinkedTreeNode(*troot, left, leftLen, right, rightLen);
                        nodeLen = DEFAULT_BRANCH_LENGTH_MARKER;

                        left  = NULp;
                        right = NULp;
                    }
                    else {
                        setExpectedError("one of ',)'");
                    }
                }

                if (right) {
                    right->forget_origin();
                    destroy(right, troot);
                }
                if (error && node) {
                    node->forget_origin();
                    destroy(node, troot);
                    node = NULp;
                }

                break;
            }

            default:
                setExpectedError("one of ',)'");
                break;
        }
        if (left) {
            left->forget_origin();
            destroy(left, troot);
        }
    }

    if (!error) drop_tree_char(')');

    tree_assert(contradicted(node, error));
    return node;
}

static TreeReader *consumingReader = NULp;
static void warningToConsumingReader(const char *msg) {
    if (consumingReader) consumingReader->add_warning(msg);
}

TreeNode *TREE_load(const char *path, TreeRoot *troot, char **commentPtr, bool allow_length_scaling, char **warningPtr) {
    /* Load a newick compatible tree from file 'path',
       if commentPtr is specified -> set it to a malloc copy of all concatenated comments found in tree file
       if warningPtr is specified -> set it to a malloc copy of any warnings occurring during tree-load (e.g. autoscale- or informational warnings)
    */

    TreeNode *tree     = NULp;
    FILE     *input    = fopen(path, "rt");
    GB_ERROR  error    = NULp;
    bool      own_root = true;

    if (!input) {
        error = GBS_global_string("No such file: %s", path);
    }
    else {
        const char *name_only = strrchr(path, '/');
        if (name_only) ++name_only;
        else        name_only = path;

        TreeReader reader(input, name_only, troot);
        if (!reader.error) {
            tree = reader.load();
            if (tree) own_root = false;
        }
        fclose(input);

        if      (reader.error)            error = reader.error;
        else if (tree && tree->is_leaf()) error = "tree is too small (need at least 2 species)";

        if (error) {
            destroy(tree);
            tree = NULp;
        }

        if (tree) {
            double bootstrap_scale = 1.0;
            double branchlen_scale = 1.0;

            if (reader.get_max_found_bootstrap() >= 101.0) { // bootstrap values were given in percent
                bootstrap_scale = 0.01;
                reader.add_warningf("Auto-scaling bootstrap values by factor %.2f (max. found bootstrap was %5.2f)",
                                    bootstrap_scale, reader.get_max_found_bootstrap());
            }
            if (reader.get_max_found_branchlen() >= 1.1) { // assume branchlengths have range [0;100]
                if (allow_length_scaling) {
                    branchlen_scale = 0.01;
                    reader.add_warningf("Auto-scaling branchlengths by factor %.2f (max. found branchlength = %.2f)\n"
                                        "(use ARB/Tree/Modify branches/Scale branchlengths with factor %.2f to undo auto-scaling)",
                                        branchlen_scale, reader.get_max_found_branchlen(), 1.0/branchlen_scale);
                }
            }

            {
                // scale bootstraps and branchlengths:
                LocallyModify<TreeReader*> assign_consumer(consumingReader, &reader);
                TREE_scale(tree, branchlen_scale, bootstrap_scale, warningToConsumingReader);
            }

            // correct root remark(s)
            if (!tree->is_leaf()) {
                tree_assert(tree == tree->get_root_node());
                // @@@ correction could be done in parse_bootstrap + just call it once here: ../../ARBDB/TreeNode.cxx@CORR_ROOT_REM
                TreeNode *left_son  = tree->get_leftson();
                TreeNode *right_son = tree->get_rightson();

                if (!left_son->is_leaf() && !right_son->is_leaf()) {
                    const char *left_rem  = left_son->get_remark();
                    const char *right_rem = right_son->get_remark();

                    if (left_rem != right_rem) { // not identical
                        TreeNode *son_with_remark = left_rem ? left_son : right_son;
                        TreeNode *other_son       = left_rem ? right_son : left_son;

                        tree_assert(son_with_remark->is_inner_node_with_remark());
                        other_son->use_as_remark(son_with_remark->get_remark_ptr());

                        left_rem  = left_son->get_remark();
                        right_rem = right_son->get_remark();
                    }
                }
                tree_assert(tree->has_valid_root_remarks());
            }

            if (warningPtr) {
                reader.add_drop_summary_warning_if_suppressed();
                const char *wmsg      = reader.get_warnings();
                if (wmsg) *warningPtr = strdup(wmsg);
            }

            if (commentPtr) {
                char *comment = reader.takeComment();

                const char *loaded_from = GBS_global_string("Loaded from %s", path);
                freeset(comment, GBS_log_action_to(comment, loaded_from, true));

                // @@@ append warnings?

                tree_assert(!*commentPtr);
                *commentPtr = comment;
            }
        }
    }

    tree_assert(tree||error);
    if (error) {
        GB_export_errorf("Import tree: %s", error);
        tree_assert(!tree);
        if (own_root) troot->delete_by_node();
    }

    return tree;
}

GB_ERROR TREE_load_to_db(GBDATA *gb_main, const char *treefile, const char *tree_name, const LabelTranslator& translator) {
    GB_ERROR error = NULp;

    char *warnings     = NULp;
    char *tree_comment = NULp;

    TreeNode *tree = TREE_load(treefile, new SimpleRoot, &tree_comment, true, &warnings);

    if (!tree) error = GB_await_error();
    else {
        if (warnings) GBT_message(gb_main, warnings);

        error = TREE_translate_labels(gb_main, tree, translator);

        if (!error) {
            GB_transaction ta(gb_main);
            error = GBT_write_tree_with_remark(gb_main, tree_name, tree, tree_comment);
            error = ta.close(error);
        }

        destroy(tree);
    }

    free(warnings);
    free(tree_comment);

    return error;
}

// --------------------------------------------------------------------------------

#ifdef UNIT_TESTS
#ifndef TEST_UNIT_H
#include <test_unit.h>
#endif

static arb_test::match_expectation parsing_label_succeeds(const char *label, bool result_expected, const double bootstrap_expected, const char *remark_expected, const char *groupname_expected) {
    using namespace   arb_test;
    expectation_group expected;

    const double UNCHANGED_VALUE = -666.66;

    double      bootstrap = UNCHANGED_VALUE;
    char       *remark    = (char*)"xxx";
    const char *groupname = label;

    bool result = parse_treelabel(groupname, bootstrap, remark);

    expected.add(that(result).is_equal_to(result_expected));
    expected.add(that(groupname).is_equal_to(groupname_expected));

    const double EPSILON = 0.000001;
    tree_assert(!epsilon_similar(EPSILON)(bootstrap_expected, UNCHANGED_VALUE));

    expected.add(that(bootstrap).fulfills(epsilon_similar(EPSILON), bootstrap_expected));
    expected.add(that(remark).is_equal_to(remark_expected));

    freenull(remark);

    return all().ofgroup(expected);
}

const double ZERO = 0.0;

#define TEST_PARSES_LABEL_AS_EMPTY(label)                   TEST_EXPECTATION(parsing_label_succeeds(label, false, ZERO, NULp, NULp))
#define TEST_PARSES_LABEL_AS_PLAIN_GROUP(label,group)       TEST_EXPECTATION(parsing_label_succeeds(label, false, ZERO, NULp, group))
#define TEST_PARSES_LABEL_AS_PLAIN_BS(label,bs)             TEST_EXPECTATION(parsing_label_succeeds(label, true,  bs,   NULp, NULp))
#define TEST_PARSES_LABEL_AS_PLAIN_REM(label,rem)           TEST_EXPECTATION(parsing_label_succeeds(label, false, ZERO, rem,  NULp))
#define TEST_PARSES_LABEL_AS_BS_AND_GROUP(label,bs,group)   TEST_EXPECTATION(parsing_label_succeeds(label, true,  bs,   NULp, group))
#define TEST_PARSES_LABEL_AS_REM_AND_GROUP(label,rem,group) TEST_EXPECTATION(parsing_label_succeeds(label, false, ZERO, rem,  group))

#define TEST_PARSES_LABEL_AS_PLAIN_REM__BROKEN(label,rem)     TEST_EXPECTATION__BROKEN_SIMPLE(parsing_label_succeeds(label, false, ZERO, rem,  NULp))
#define TEST_PARSES_LABEL_AS_PLAIN_GROUP__BROKEN(label,group) TEST_EXPECTATION__BROKEN_SIMPLE(parsing_label_succeeds(label, false, ZERO, NULp, group))
#define TEST_PARSES_LABEL_AS_PLAIN_BS__BROKEN(label,bs)       TEST_EXPECTATION__BROKEN_SIMPLE(parsing_label_succeeds(label, true,  bs,   NULp, NULp))

#define TEST_PARSES_LABEL_AS_REM_AND_GROUP__BROKEN(label,rem_wanted,group_wanted,rem_got,group_got)     \
    TEST_EXPECTATION__BROKEN(parsing_label_succeeds(label, false, ZERO, rem_wanted,  group_wanted),     \
                             parsing_label_succeeds(label, false, ZERO, rem_got,     group_got))

void TEST_treelabel_parser() {
    TEST_PARSES_LABEL_AS_PLAIN_GROUP("group", "group");
    TEST_PARSES_LABEL_AS_PLAIN_GROUP("77x",   "77x");

    TEST_PARSES_LABEL_AS_PLAIN_BS("33%",       0.33);
    TEST_PARSES_LABEL_AS_PLAIN_BS("0.123",     0.123);
    TEST_PARSES_LABEL_AS_PLAIN_BS("0.456",     0.456);
    TEST_PARSES_LABEL_AS_PLAIN_BS("93.27",     93.27);
    TEST_PARSES_LABEL_AS_PLAIN_BS("93.27%",    0.9327);
    TEST_PARSES_LABEL_AS_PLAIN_BS("93.27618%", 0.932762);

    TEST_PARSES_LABEL_AS_BS_AND_GROUP ("24%:group",       0.24,     "group");
    TEST_PARSES_LABEL_AS_REM_AND_GROUP("remark:group",    "remark", "group");
    TEST_PARSES_LABEL_AS_REM_AND_GROUP("remark:456group", "remark", "456group");

    TEST_PARSES_LABEL_AS_REM_AND_GROUP("123remark:group",    "123remark", "group");
    TEST_PARSES_LABEL_AS_REM_AND_GROUP("123remark:456group", "123remark", "456group");

    TEST_PARSES_LABEL_AS_REM_AND_GROUP("a:b:c", "a", "b:c"); // group name is invalid (but not reported here); reported in testcase .@REMGRPBAD

    // test empty group and/or remark:
    TEST_PARSES_LABEL_AS_PLAIN_REM("remark:",       "remark");
    TEST_PARSES_LABEL_AS_PLAIN_REM("12.789remark:", "12.789remark");

    TEST_PARSES_LABEL_AS_PLAIN_BS("93.27%:", 0.9327);
    TEST_PARSES_LABEL_AS_PLAIN_BS("14%:", 0.14);
    TEST_PARSES_LABEL_AS_PLAIN_BS("81:", 81.0);

    TEST_PARSES_LABEL_AS_PLAIN_GROUP(":group", "group");

    TEST_PARSES_LABEL_AS_EMPTY(":");
    TEST_PARSES_LABEL_AS_EMPTY("");
}

static TreeNode *loadFromFileContaining(const char *treeString, char **warningsPtr) {
    const char *filename = "trees/tmp.tree";
    FILE       *out      = fopen(filename, "wt");
    TreeNode   *tree     = NULp;

    if (out) {
        fputs(treeString, out);
        fclose(out);
        tree = TREE_load(filename, new SimpleRoot, NULp, false, warningsPtr);
    }
    else {
        GB_export_IO_error("save tree", filename);
    }

    return tree;
}

static arb_test::match_expectation loading_tree_failed_with(TreeNode *tree, const char *errpart) {
    using namespace   arb_test;
    expectation_group expected;

    expected.add(that(tree).is_equal_to_NULL());
    expected.add(that(GB_have_error()).is_equal_to(true));
    if (GB_have_error()) {
        expected.add(that(GB_await_error()).does_contain(errpart));
    }
    return all().ofgroup(expected);
}

static arb_test::match_expectation loading_tree_succeeds(TreeNode *tree, const char *newick_expected, NewickFormat format) {
    using namespace   arb_test;
    expectation_group expected;

    expected.add(that(tree).does_differ_from_NULL());
    expected.add(that(GB_get_error()).is_equal_to_NULL());
    if (!GB_have_error() && tree) {
        expected.add(that(tree->get_root_node()->has_valid_root_remarks()).is_equal_to(true));
        char *newick = GBT_tree_2_newick(tree, format, false);
        expected.add(that(newick).is_equal_to(newick_expected));
        free(newick);
    }
    return all().ofgroup(expected);
}

#define TEST_EXPECT_TREELOAD_FAILED_WITH(tree,errpart)         TEST_EXPECTATION(loading_tree_failed_with(tree, errpart))
#define TEST_EXPECT_TREELOAD_FAILED_WITH__BROKEN(tree,errpart) TEST_EXPECTATION__BROKEN(loading_tree_failed_with(tree, errpart))

#define TEST_EXPECT_TREELOAD(tree,newick)         TEST_EXPECTATION(loading_tree_succeeds(tree,newick,expectedFormat))
#define TEST_EXPECT_TREELOAD__BROKEN(tree,newick) TEST_EXPECTATION__BROKEN(loading_tree_succeeds(tree,newick,expectedFormat))

#define TEST_EXPECT_TREEFILE_FAILS_WITH(name,errpart) do {                              \
        TreeNode *tree = TREE_load(name, new SimpleRoot, NULp, false, NULp);           \
        TEST_EXPECT_TREELOAD_FAILED_WITH(tree, errpart);                                \
    } while(0)

#define TEST_EXPECT_TREESTRING_FAILS_WITH(treeString,errpart) do {      \
        TreeNode *tree = loadFromFileContaining(treeString, NULp);      \
        TEST_EXPECT_TREELOAD_FAILED_WITH(tree, errpart);                \
    } while(0)

// argument 'newick' is vs regression only!
#define TEST_EXPECT_TREESTRING_FAILS_WITH__BROKEN(treeString,errpart,newick) do {       \
        char     *warnings = NULp;                                                      \
        TreeNode *tree     = loadFromFileContaining(treeString, &warnings);             \
        TEST_EXPECT_TREELOAD_FAILED_WITH__BROKEN(tree, errpart);                        \
        TEST_EXPECT_TREELOAD(tree, newick);                                             \
        TEST_EXPECT_NULL(warnings);                                                     \
        delete tree;                                                                    \
        free(warnings);                                                                 \
    } while(0)

#define TEST_EXPECT_TREESTRING_OK(treeString,newick) do {                       \
        char     *warnings = NULp;                                              \
        TreeNode *tree     = loadFromFileContaining(treeString, &warnings);     \
        TEST_EXPECT_TREELOAD(tree, newick);                                     \
        TEST_EXPECT_NULL(warnings);                                             \
        destroy(tree);                                                          \
        free(warnings);                                                         \
    } while(0)

static arb_test::match_expectation reports_these_warnings(const char *report, const char *expectedWarnings) {
    // both arguments are multiple lines joined with \n (line-amount: report==expectedWarnings)
    ConstStrArray reportParts;
    ConstStrArray warningParts;

    GBT_split_string(reportParts, report, "\n", SPLIT_DROPEMPTY);
    GBT_split_string(warningParts, expectedWarnings, "\n", SPLIT_DROPEMPTY);

    using namespace   arb_test;
    expectation_group expected;

    size_t reportCount = reportParts.size();
    size_t warningCount = warningParts.size();

    expected.add(that(reportCount).is_more_or_equal(warningCount));

    bool reportMatched[reportCount];
    bool warningMatched[warningCount];

    for (int w = 0; warningParts[w]; ++w) {
        const char *warningPart = warningParts[w];
        warningMatched[w]       = false;

        for (int r = 0; reportParts[r]; ++r) {
            if (!w) reportMatched[r] = false;
            else if (reportMatched[r]) continue;
            const char *reportPart = reportParts[r];
            if (strstr(reportPart, warningPart)) {
                expected.add(that(reportPart).does_contain(warningPart));
                reportMatched[r]  = true;
                warningMatched[w] = true;
            }
        }
    }

    for (int w = 0; warningParts[w]; ++w) if (!warningMatched[w]) expected.add(that(warningParts[w]).is_equal_to("<unreported>"));
    for (int r = 0; reportParts[r]; ++r) if (!reportMatched[r]) expected.add(that(reportParts[r]).is_equal_to("<reported, but not expected>"));

    return all().ofgroup(expected);
}

#define TEST_EXPECT_HAVE_WARNINGS(warningsReported, expectedWarnParts) do {             \
        TEST_REJECT_NULL(warnings);                                                     \
        TEST_EXPECTATION(reports_these_warnings(warningsReported, expectedWarnParts));  \
    } while (0)

#define TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS(treeString,newick,expectedWarnParts) do { \
        char     *warnings = NULp;                                                              \
        TreeNode *tree     = loadFromFileContaining(treeString, &warnings);                     \
        TEST_EXPECT_TREELOAD(tree, newick);                                                     \
        TEST_EXPECT_HAVE_WARNINGS(warnings, expectedWarnParts);                                 \
        destroy(tree);                                                                          \
        free(warnings);                                                                         \
    } while(0)

#define TEST_EXPECT_TREESTRING_OK__BROKEN(treeString,newick) do {       \
        TreeNode *tree = loadFromFileContaining(treeString, NULp);      \
        TEST_EXPECT_TREELOAD__BROKEN(tree, newick);                     \
    } while(0)

#define LF "\n"

void TEST_load_tree() {
    // just are few tests covering most of this module.
    // more load tests are in ../../TOOLS/arb_test.cxx@TEST_SLOW_arb_read_tree

    NewickFormat expectedFormat = nSIMPLE;

    // simple succeeding tree load
    {
        char     *comment = NULp;
        TreeNode *tree    = TREE_load("trees/test.tree", new SimpleRoot, &comment, false, NULp);
        // -> ../../UNIT_TESTER/run/trees/test.tree

        TEST_EXPECT_TREELOAD(tree, "(((s1,s2),(s3,s 4)),(s5,s-6));");
        if (tree) {
            TEST_REJECT_NULL(comment);
            TEST_EXPECT_CONTAINS(comment,
                                 // comment part from treefile:
                                 "tree covering most of tree reader code\n"
                                 "comment contains [extra brackets] inside comment\n");
            TEST_EXPECT_CONTAINS(comment,
                                 // comment as appended by load:
                                 ": Loaded from trees/test.tree\n");
        }
        free(comment);
        destroy(tree);
    }

    // detailed load tests (checking branchlengths and nodenames)
    {
        const char *treestring[] = {
            "(node1,node2)rootgroup;",             // [0] tree with a named root
            "(node1:0.00,(node2, node3:0.57)):0;", // [1] test tree lengths (esp. length zero)
            "(((((a))single)), ((b, c)17%:0.2));", // [2] test single-node-subtree name-conflict

            "((a,b)17,(c,d)33.3,(e,f)12.5:0.2);",             // [3] test bootstraps
            "((a,b)G,(c,d)H,(e,f)I:0.2);",                    // [4] test groupnames w/o bootstraps
            "((a,b)'17:G',(c,d)'33.3:H',(e,f)'12.5:I':0.2);", // [5] test groupnames with bootstraps
            "((a,b)17G,(c,d)33.3H,(e,f)12.5I:0.2)",           // [6] test groupnames + bootstraps w/o separator -> interpreted as groupname

            "((a,b)'17%:G',(c,d)'33.3%:H',(e,f)'12.5%:I':0.2);",  // [7] test bootstraps with percent spec
            "((a,b)'0.17:G',(c,d)'0.333:H',(e,f)'0.125:I':0.2);", // [8] test bootstraps in range [0..1]
        };

        const char *EXPECTED_NEWICK_3TO8 = "(((a,b),(c,d)),(e,f));";

        const char *expected_newick[] = {
            "(node1,node2);",
            "(node1,(node2,node3));",
            "(a,(b,c));",

            EXPECTED_NEWICK_3TO8,
            EXPECTED_NEWICK_3TO8,
            EXPECTED_NEWICK_3TO8,
            EXPECTED_NEWICK_3TO8,

            EXPECTED_NEWICK_3TO8,
            EXPECTED_NEWICK_3TO8,
        };
        const char *expected_warnings[] = {
            NULp,
            NULp,
            "Dropped group name specified for a single-node-subtree" LF "Ignored invalid bootstrap '17%' at root edge leading to a leaf",

            "Auto-scaling bootstrap values by factor 0.01" LF "Using comment '1250%' on both sides of root-edge",
            NULp,
            "Auto-scaling bootstrap values by factor 0.01" LF "Using comment '1250%' on both sides of root-edge",
            NULp,

            "Using comment '13%' on both sides of root-edge", // no auto-scaling shall occur here (bootstraps are already specified as percent)
            "Using comment '13%' on both sides of root-edge", // no auto-scaling shall occur here (bootstraps are in [0..1])
        };

        STATIC_ASSERT(ARRAY_ELEMS(expected_newick) == ARRAY_ELEMS(treestring));
        STATIC_ASSERT(ARRAY_ELEMS(expected_warnings) == ARRAY_ELEMS(treestring));

        for (size_t i = 0; i<ARRAY_ELEMS(treestring); ++i) {
            TEST_ANNOTATE(GBS_global_string("for tree #%zu = '%s'", i, treestring[i]));
            char     *warnings = NULp;
            TreeNode *tree     = loadFromFileContaining(treestring[i], &warnings);
            TEST_EXPECT_TREELOAD(tree, expected_newick[i]);
            switch (i) {
                case 0:
                    TEST_EXPECT_EQUAL(tree->name, "rootgroup");
                    break;
                case 1:
                    TEST_EXPECT_EQUAL(tree->leftlen, 0);
                    TEST_EXPECT_EQUAL(tree->rightlen, DEFAULT_BRANCH_LENGTH);
                    TEST_EXPECT_EQUAL(tree->rightson->rightlen, 0.57);
                    break;
                case 2:
                    TEST_EXPECT_NULL(tree->rightson->name);
                    TEST_EXPECT_NULL(tree->rightson->get_remark());
                    TEST_EXPECT_EQUAL(tree->rightlen, 0.2);
                    TEST_EXPECT_EQUAL(tree->leftlen, DEFAULT_BRANCH_LENGTH);
                    break;

                case 3:
                case 4:
                case 5:
                case 6:
                case 7:
                case 8:
                    // check bootstraps
                    switch (i) {
                        case 4:
                        case 6:
                            TEST_EXPECT_NULL(tree->leftson->leftson->get_remark());
                            TEST_EXPECT_NULL(tree->leftson->rightson->get_remark());
                            TEST_EXPECT_NULL(tree->leftson->get_remark());
                            TEST_EXPECT_NULL(tree->rightson->get_remark());
                            break;
                        case 3:
                        case 5:
                        case 7:
                        case 8:
                            // test bootstraps with percent-specifications are parsed correctly
                            TEST_EXPECT_EQUAL(tree->leftson->leftson->get_remark(),  "17%");
                            TEST_EXPECT_EQUAL(tree->leftson->rightson->get_remark(), "33%");
                            TEST_EXPECT_EQUAL(tree->rightson->get_remark(),          "13%");
                            TEST_EXPECT_EQUAL(tree->leftson->get_remark(),           "13%");
                            break;
                        default:
                            TEST_REJECT(true); // unhandled tree
                            break;
                    }

                    // check node-names
                    TEST_EXPECT_NULL(tree->name);
                    TEST_EXPECT_NULL(tree->leftson->name);
                    switch (i) {
                        case 6:
                            // check un-separated digits are treated as strange names
                            // (previously these were accepted as bootstraps)
                            TEST_EXPECT_EQUAL(tree->leftson->leftson->name,  "17G");
                            TEST_EXPECT_EQUAL(tree->leftson->rightson->name, "33.3H");
                            TEST_EXPECT_EQUAL(tree->rightson->name,          "12.5I");
                            break;
                        case 4:
                        case 5:
                        case 8:
                        case 7:
                            TEST_EXPECT_EQUAL(tree->leftson->leftson->name,         "G");
                            TEST_EXPECT_EQUAL(tree->leftson->rightson->name,        "H");
                            TEST_EXPECT_EQUAL(tree->rightson->name,                 "I");
                            break;
                        case 3:
                            TEST_EXPECT_NULL(tree->leftson->leftson->name);
                            TEST_EXPECT_NULL(tree->leftson->rightson->name);
                            TEST_EXPECT_NULL(tree->rightson->name);
                            break;
                        default:
                            TEST_REJECT(true); // unhandled tree
                            break;
                    }

                    // expect_no_lengths:
                    TEST_EXPECT_EQUAL(tree->leftlen,           0); // multifurcation
                    TEST_EXPECT_EQUAL(tree->leftson->leftlen,  DEFAULT_BRANCH_LENGTH);
                    TEST_EXPECT_EQUAL(tree->leftson->rightlen, DEFAULT_BRANCH_LENGTH);
                    TEST_EXPECT_EQUAL(tree->rightlen,          0.2);
                    break;

                default:
                    TEST_REJECT(true); // unhandled tree
                    break;
            }
            if (expected_warnings[i]) {
                TEST_EXPECT_HAVE_WARNINGS(warnings, expected_warnings[i]);
            }
            else                      {
                TEST_EXPECT_NULL(warnings);
            }
            free(warnings);
            destroy(tree);
        }

        TEST_ANNOTATE(NULp);
    }

    expectedFormat = nSIMPLE;

    // test valid trees with strange behavior
    TEST_EXPECT_TREESTRING_OK("(,);",      "(unnamed1,unnamed2);"); // tree with 2 unamed species (weird, but ok)
    TEST_EXPECT_TREESTRING_OK("((a,));  ", "(a,unnamed1);");
    TEST_EXPECT_TREESTRING_OK("((,b));  ", "(unnamed1,b);");

    TEST_EXPECT_TREESTRING_OK("( a, (b,(c),d), (e,(f)) );", "((a,((b,c),d)),(e,f));");
    TEST_EXPECT_TREESTRING_OK("(((((a)))), ((b, c)));",     "(a,(b,c));");

    // test group names:
    expectedFormat = NewickFormat(nGROUP|nREMARK);

    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("( (a), ((((b),(c),(d))'rem:')'duprem:group')dupgroup, ((e),(f)) );",
                                            "((a,((b,c),d)'rem:group'),(e,f));",
                                            "Dropped remark redefinition 'duprem' (edge already labeled as 'rem')" LF
                                            "Dropped group name redefinition 'dupgroup' (furcation already named 'group')");

    // test limits where boostraps can/cannot occur
    expectedFormat = nREMARK;

    TEST_EXPECT_TREESTRING_OK              ("(a,b);",                "(a,b);");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(a,b)17%;",             "(a,b);",          "Remark specified for root-node ('17%') has been ignored");

    TEST_EXPECT_TREESTRING_OK              ("((a,b),c);",            "((a,b),c);");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("((a,b)23%,c);",         "((a,b),c);",      "Ignored invalid bootstrap '23%' at root edge leading to a leaf");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(c,(a,b)23%);",         "(c,(a,b));",      "Ignored invalid bootstrap '23%' at root edge leading to a leaf");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(c,(a,b)23%)99%;",      "(c,(a,b));",
                                            "Remark specified for root-node ('99%') has been ignored" LF
                                            "Ignored invalid bootstrap '23%' at root edge leading to a leaf");

    TEST_EXPECT_TREESTRING_OK              ("((a,b),(c,d));",        "((a,b),(c,d));");
    // test conflicting bootstraps specified for root-edge:
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("((a,b)17%,(c,d)39%);",      "((a,b)'28%',(c,d)'28%');", "Root-edge has conflicting support values '17%' and '39%' -> using average '28%'");
    TEST_EXPECT_TREESTRING_OK              ("((a,b)0.333,(c,d)0.334);",  "((a,b)'33%',(c,d)'33%');"); // difference between support values is below 1% -> assume numeric error + correct silently
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("((a,b)0.321,(c,d)0.345);",  "((a,b)'34%',(c,d)'34%');", "Root-edge has conflicting support values '32%' and '35%' -> using average '34%'"); // unintuitive. this is caused by order of auto-scaling, rounding and root-correction

    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("((a,b)17%,(c,d));",     "((a,b)'17%',(c,d)'17%');", "Using comment '17%' on both sides of root-edge");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("((a,b),(c,d)17%);",     "((a,b)'17%',(c,d)'17%');", "Using comment '17%' on both sides of root-edge");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("((a,b)17%,(c,d)100%);", "((a,b)'17%',(c,d)'17%');", "Using comment '17%' on both sides of root-edge"); // dropping 100% is a bit surprising
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("((a,b)100%,(c,d)17%);", "((a,b)'17%',(c,d)'17%');", "Using comment '17%' on both sides of root-edge"); // dito
    TEST_EXPECT_TREESTRING_OK              ("((a,b)100%,(c,d)100%);","((a,b),(c,d));");

    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("((a,(b,c))32%,d);",     "((a,(b,c)),d);",           "Ignored invalid bootstrap '32%' at root edge leading to a leaf");
    TEST_EXPECT_TREESTRING_OK              ("((a,(b,c)41%),d);",     "((a,(b,c)'41%'),d);"); // ok. BS located at sole inner branch between (b,c) and (a,d)

    // test various invalid information at single-node subtrees:
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(((a)41%,(b,c)),d);",    "((a,(b,c)),d);",          "Dropped support-value ('0.41') specified for a single-node-subtree");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(((b,c),(a)0.41),d);",   "(((b,c),a),d);",          "Dropped support-value ('0.41') specified for a single-node-subtree");

    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(((b,c),(a)name),d);",          "(((b,c),a),d);",   "Dropped group name specified for a single-node-subtree ('name')");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(((a)'name',(b,c)),d);",        "((a,(b,c)),d);",   "Dropped group name specified for a single-node-subtree ('name')");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(((b,c),(a)'35%:name'),d);",    "(((b,c),a),d);",   "Dropped group name specified for a single-node-subtree ('name')" LF "Dropped support-value ('0.35') specified for a single-node-subtree");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(((a)'remark:name',(b,c)),d);", "((a,(b,c)),d);",   "Dropped group name specified for a single-node-subtree ('name')" LF "Dropped remark ('remark') specified for a single-node-subtree");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(((b,c),(a)'remark:'),d);",     "(((b,c),a),d);",   "Dropped remark ('remark') specified for a single-node-subtree");

    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("((a, b)25)20;", "(a,b);",
                                            "Dropped support-value redefinition '20' (edge already labeled as '2500%')" LF
                                            "Remark specified for root-node ('2500%') has been ignored" LF
                                            "Auto-scaling bootstrap values by factor 0.01 (max. found bootstrap was 2500.00)");
    expectedFormat = nALL;

    // check some branchlengths:
    TEST_EXPECT_TREESTRING_OK("((a:0,b:5):-.9,(c:.3, d:-.0001));", "((a:0,b:5):-0.9,(c:.3,d:0):.1);");
    TEST_EXPECT_TREESTRING_OK("(((a,b),(c,d)),((e,f),(g,h)));",    "(((a:.1,b:.1):.1,(c:.1,d:.1):.1):.1,((e:.1,f:.1):.1,(g:.1,h:.1):.1):.1);");

    TEST_EXPECT_TREESTRING_OK("(((a,b)'0.25:G':.6,(c,d)'17%':.5),((e,f)'r:g':.2,(g,h)g:.3));",
                              "(((a:.1,b:.1)'25%:G':.6,(c:.1,d:.1)'17%':.5):.1,((e:.1,f:.1)'r:g':.2,(g:.1,h:.1)'g':.3):.1);");

    TEST_EXPECT_TREESTRING_OK("(:.2,:2);", "(unnamed1:.2,unnamed2:2);"); // tree with 2 unamed species but length (weird, but ok)

    // test unacceptable trees
    expectedFormat = nSIMPLE;
    {
        const char *tooSmallTree[] = {
            "();",
            "()",
            ";",
            "",
            "(one)",
            "((((()))));",
            "(((((one)))));",
        };

        for (size_t i = 0; i<ARRAY_ELEMS(tooSmallTree); ++i) {
            TEST_ANNOTATE(GBS_global_string("for tree #%zu = '%s'", i, tooSmallTree[i]));
            TreeNode *tree = loadFromFileContaining(tooSmallTree[i], NULp);
            TEST_EXPECT_TREELOAD_FAILED_WITH(tree, "tree is too small");
        }
        TEST_ANNOTATE(NULp);
    }

    // test invalid trees
    TEST_EXPECT_TREESTRING_FAILS_WITH("(;);", "Expected one of ',)'");

    TEST_EXPECT_TREESTRING_FAILS_WITH("(17",      "Expected one of ',)' while end-of-file was reached");
    TEST_EXPECT_TREESTRING_FAILS_WITH("((((",     "Expected one of ',)' while end-of-file was reached");
    TEST_EXPECT_TREESTRING_FAILS_WITH("(a, 'b",   "Expected one of ',)' while end-of-file was reached");
    TEST_EXPECT_TREESTRING_FAILS_WITH("((a,  ",   "Expected one of ',)' while end-of-file was reached");
    TEST_EXPECT_TREESTRING_FAILS_WITH("((a,'b  ", "Expected one of ',)' while end-of-file was reached");

    TEST_EXPECT_TREESTRING_FAILS_WITH("(a, b:5::::",  "Unexpected ':' (already read a branchlength) while looking at '::::<EOF>'");
    TEST_EXPECT_TREESTRING_FAILS_WITH("(a, b:5:c:d",  "Unexpected ':' (already read a branchlength) while looking at ':c:d<EOF>'");
    TEST_EXPECT_TREESTRING_FAILS_WITH("(a, b:5:c:d)", "Unexpected ':' (already read a branchlength) while looking at ':c:d)<EOF>'");

    TEST_EXPECT_TREESTRING_FAILS_WITH("[unclosed\ncomment",         "while reading comment");
    TEST_EXPECT_TREESTRING_FAILS_WITH("[unclosed\ncomment [ bla ]", "while reading comment");

    TEST_EXPECT_TREESTRING_FAILS_WITH("(a, b:d)", "Expected valid length while looking at 'd)<EOF>'");

    TEST_EXPECT_TREESTRING_FAILS_WITH("((a:0,b:.),c:.3);", "cannot convert '.' to float while looking at '),c:.3);<EOF>'");

    expectedFormat = nALL;

    {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#define FAILS_WITH(err) NULp, NULp, err
        struct NodeInfo {
            const char *input;
            const char *left;
            const char *right;
            const char *failsWith; // only checks failure (warnings are ignored here)
        } labelTest[] = {
            // no/empty node-info
            { "",     ":.1" }, // .1 is the default length used as fallback
            { "''",   ":.1" },
            { "\"\"", ":.1" },

            { "grp",    ":.1", "'grp':.1" }, // only group -> stored at right son of root
            { ":2",     ":.1", ":2" },       // only length
            { "'rem:'", "'rem:':.1" },       // only remark

            { "17.2",  "'17%':.1" }, // only bootstrap -> stored at both sons of root
            { "17.2%", "'17%':.1" }, // only bootstrap -> stored at both sons of root
            { "32.7",  "'33%':.1" }, // only bootstrap -> stored at both sons of root
            { "32.7%", "'33%':.1" }, // only bootstrap -> stored at both sons of root
            { "0.572", "'57%':.1" }, // only bootstrap -> stored at both sons of root
            { "0.725", "'73%':.1" }, // only bootstrap -> stored at both sons of root


            { "grp:2",     ":.1",       "'grp':2" },  // group+length -> stored at right son of root
            { "'grp':2",   ":.1",       "'grp':2" },  // quoted group+length -> stored at right son of root
            { "'rem:':2",  "'rem:':.1", "'rem:':2" }, // remark+length
            { "0.725:2",   "'73%':.1",  "'73%':2" },  // bootstrap+length -> only bootstrap is stored at both sons of root!
            { "'0.725':2", "'73%':.1",  "'73%':2" },  // quoted bootstrap+length -> only bootstrap is stored at both sons of root!

            { "'rem:grp'", "'rem:':.1", "'rem:grp':.1" }, // remark+group -> stored at right son of root
            { "'3%:grp'",  "'3%':.1",   "'3%:grp':.1" },  // bootstrap+group -> only bootstrap is stored at both sons of root!

            { "'3%:grp':2", "'3%':.1", "'3%:grp':2" }, // bootstrap+group+length -> only bootstrap is stored at both sons of root!
            { "':grp':2",   ":.1",     "'grp':2" },    // empty remark/bootstrap+group+length
            { "'3%:':2",    "'3%':.1", "'3%':2" },     // bootstrap+empty group+length -> only bootstrap is stored at both sons of root!
            { "':':2",      ":.1",     ":2" },         // ignores empty bootstrap+empty group; only group+length stored at right son
            { "':'",        ":.1" },                   // ignores empty bootstrap+empty group

            // custom remarks to display 'shalrt / ufboot' bootstraps
            { "'95/80:group'",         "'95/80:':.1",         "'95/80:group':.1" },
            { "'95/80:'",              "'95/80:':.1"          },
            { "'98.6 / 89.3:group'",   "'98.6 / 89.3:':.1",   "'98.6 / 89.3:group':.1" },
            { "'98.6 / 89.3:'",        "'98.6 / 89.3:':.1"    },
            { "'98.6% / 89.3%:group'", "'98.6% / 89.3%:':.1", "'98.6% / 89.3%:group':.1" },
            { "'98.6% / 89.3%:'",      "'98.6% / 89.3%:':.1", },

            // wanted failures:
            { "grp:",       FAILS_WITH("Expected valid length while looking at ');<EOF>'") },
            { "2%:",        FAILS_WITH("Expected valid length while looking at ');<EOF>'") },
            { ".2:",        FAILS_WITH("Expected valid length while looking at ');<EOF>'") },
            { "'rem:grp':", FAILS_WITH("Expected valid length while looking at ');<EOF>'") },
            { "'rem:':",    FAILS_WITH("Expected valid length while looking at ');<EOF>'") },
            { "'':",        FAILS_WITH("Expected valid length while looking at ');<EOF>'") },

            { "'2%:4%'", FAILS_WITH("Invalid group name '4%' (would be misinterpreted as plain inner node with a support value of 4% if re-imported from newick file) while looking at ');<EOF>'") },
            { "'2:4'",   FAILS_WITH("Invalid group name '4' (would be misinterpreted as plain inner node with a support value of 4% if re-imported from newick file) while looking at ');<EOF>'") },
            { "'.2:.4'", FAILS_WITH("Invalid group name '.4' (would be misinterpreted as plain inner node with a support value of 40% if re-imported from newick file) while looking at ');<EOF>'") },

            { "'rem:grp:bad'", FAILS_WITH("Invalid group name 'grp:bad' (would be misinterpreted as group named 'bad' with remark 'grp' if re-imported from newick file) while looking at ');<EOF>'") }, // .@REMGRPBAD

            // unwanted failures:
            { "'.55:'",    "'55%':.1" },
            { "'66%:'",    "'66%':.1" },
            { "'.77:':.2", "'77%':.1",   "'77%':.2" },
            { "'88%:':.3", "'88%':.1",   "'88%':.3" },
        };
#undef FAILS_WITH
#pragma GCC diagnostic pop

        for (size_t i = 0; i<ARRAY_ELEMS(labelTest); ++i) {
            const NodeInfo& nodeInfo = labelTest[i];
            TEST_ANNOTATE(GBS_global_string("for label #%zu = <%s>", i, nodeInfo.input));

            char     *treeString = GBS_global_string_copy("((a,b),(c,d)%s);", nodeInfo.input);
            TreeNode *tree       = loadFromFileContaining(treeString, NULp);

            if (nodeInfo.failsWith) {
                TEST_EXPECT_TREELOAD_FAILED_WITH(tree, nodeInfo.failsWith);
            }
            else {
                const char *leftNodeInfo  = nodeInfo.left;
                const char *rightNodeInfo = nodeInfo.right ? nodeInfo.right : leftNodeInfo;

                if (nodeInfo.right && strcmp(leftNodeInfo, rightNodeInfo) == 0) {
                    const char *UNWANTED_CONDITION = GBS_global_string("Only specify one, if both node-infos contain the same string: <%s>", leftNodeInfo);
                    TEST_EXPECT_NULL(UNWANTED_CONDITION);
                }

                char *newick = GBS_global_string_copy("((a:.1,b:.1)%s,(c:.1,d:.1)%s);", leftNodeInfo, rightNodeInfo);
                TEST_EXPECT_TREELOAD(tree, newick);
                free(newick);
                destroy(tree);
            }
            free(treeString);
        }
        TEST_ANNOTATE(NULp);
    }

    expectedFormat = nALL;

    // userland trees:
    TEST_EXPECT_TREESTRING_OK("((((A:1,B:1)'95/80:INT1':1,C:2)'99/98:INT2':0.5),(D:1,E:1)'30/40:INT3':3)'100/100';", // tree from Donovan
                              "(((A:1,B:1)'95/80:INT1':1,C:2)'99/98:INT2':.5,(D:1,E:1)'99/98:INT3':3)'100/100';"); // @@@ should warn that remark at right son of root gets overwritten

    TEST_EXPECT_TREESTRING_OK("((((A:1,B:1)'95.7% / 80.3%:':1,C:2)'99.9 / 98.5:':0.5)g1,(D:1,E:1)'30.4/40.8:g2':3);",    // tree from Donovan, slightly corrected: root-comment removed; mixed comments and remarks, use various remark formats
                              "(((A:1,B:1)'95.7% / 80.3%:':1,C:2)'99.9 / 98.5:g1':.5,(D:1,E:1)'99.9 / 98.5:g2':3);"); // @@@ should warn that remark at right son of root gets overwritten

    // questionable accepted trees / check warnings
    expectedFormat = nSIMPLE;
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(a,b):0.5", "(a,b);", "Length specified for root-node has been ignored");
    TEST_EXPECT_TREESTRING_OK_WITH_WARNINGS("(a, b))",   "(a,b);", "Unexpected input-data after tree: ')'");


    // questionable leaf names:
    TEST_EXPECT_TREESTRING_OK("(a*,b%);",           "(a*,b%);");      // really accept such names?
    TEST_EXPECT_TREESTRING_OK("(a, b:5)",           "(a,b);");
    TEST_EXPECT_TREESTRING_OK("(c,(b,'a:b:':.2));", "(c,(b,a:b:));"); // leaf-name is 'a:b:' here :-/

    // check file errors
    TEST_EXPECT_TREEFILE_FAILS_WITH("trees/nosuch.tree",    "No such file");
    TEST_EXPECT_TREEFILE_FAILS_WITH("trees/corrupted.tree", "Error reading");

    TEST_EXPECT_ZERO_OR_SHOW_ERRNO(GB_unlink("trees/tmp.tree")); // cleanup
}

#endif // UNIT_TESTS

// --------------------------------------------------------------------------------
