/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.configuration2.tree;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;

/**
 * A helper class for tests related to hierarchies of {@code ImmutableNode} objects. This class provides functionality
 * for creating test trees and accessing single nodes. It can be used by various test classes.
 */
public final class NodeStructureHelper {

    /** A pattern for parsing node keys with optional indices. */
    private static final Pattern PAT_KEY_WITH_INDEX = Pattern.compile("(\\w+)\\((\\d+)\\)");

    /** The character for splitting node path elements. */
    private static final String PATH_SEPARATOR = "/";

    /** An array with authors. */
    private static final String[] AUTHORS = {"Shakespeare", "Homer", "Simmons"};

    /** An array with the works of the test authors. */
    private static final String[][] WORKS = {{"Troilus and Cressida", "The Tempest", "A Midsummer Night's Dream"}, {"Ilias"}, {"Ilium", "Hyperion"}};

    /** An array with the personae in the works. */
    private static final String[][][] PERSONAE = {{
        // Works of Shakespeare
        {"Troilus", "Cressidia", "Ajax", "Achilles"}, {"Prospero", "Ariel"}, {"Oberon", "Titania", "Puck"}},
        {
            // Works of Homer
            {"Achilles", "Agamemnon", "Hektor"}},
        {
            // Works of Dan Simmons
            {"Hockenberry", "Achilles"}, {"Shrike", "Moneta", "Consul", "Weintraub"}}};

    /** An array with table names used for the TABLES tree. */
    private static final String[] TABLES = {"users", "documents"};

    /**
     * An array with the names of columns to be used for the TABLES tree.
     */
    private static final String[][] FIELDS = {{"uid", "uname", "firstName", "lastName", "email"},
        {"docid", "name", "creationDate", "authorID", "version", "length"}};

    /** Constant for the author attribute. */
    public static final String ATTR_AUTHOR = "author";

    /** Constant for the original value element in the personae tree. */
    public static final String ELEM_ORG_VALUE = "originalValue";

    /** Constant for the tested attribute. */
    public static final String ATTR_TESTED = "tested";

    /** The root node of the authors tree. */
    public static final ImmutableNode ROOT_AUTHORS_TREE = createAuthorsTree();

    /** The root node of the personae tree. */
    public static final ImmutableNode ROOT_PERSONAE_TREE = createPersonaeTree();

    /** The root node of the TABLES tree. */
    public static final ImmutableNode ROOT_TABLES_TREE = createTablesTree();

    /**
     * Appends a component to a node path. The component is added separated by a path separator.
     *
     * @param path the path
     * @param component the component to be added
     * @return the resulting path
     */
    public static String appendPath(final String path, final String component) {
        final StringBuilder buf = new StringBuilder(StringUtils.length(path) + StringUtils.length(component) + 1);
        buf.append(path).append(PATH_SEPARATOR).append(component);
        return buf.toString();
    }

    /**
     * Returns the name of the author at the given index.
     *
     * @param idx the index
     * @return the name of this author
     */
    public static String author(final int idx) {
        return AUTHORS[idx];
    }

    /**
     * Returns the number of authors.
     *
     * @return the number of authors
     */
    public static int authorsLength() {
        return AUTHORS.length;
    }

    /**
     * Creates a tree with a root node whose children are the test authors. Each other has his works as child nodes. Each
     * work has its personae as children.
     *
     * @return the root node of the authors tree
     */
    private static ImmutableNode createAuthorsTree() {
        final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder(AUTHORS.length);
        for (int author = 0; author < AUTHORS.length; author++) {
            final ImmutableNode.Builder authorBuilder = new ImmutableNode.Builder();
            authorBuilder.name(AUTHORS[author]);
            for (int work = 0; work < WORKS[author].length; work++) {
                final ImmutableNode.Builder workBuilder = new ImmutableNode.Builder();
                workBuilder.name(WORKS[author][work]);
                for (final String person : PERSONAE[author][work]) {
                    workBuilder.addChild(new ImmutableNode.Builder().name(person).create());
                }
                authorBuilder.addChild(workBuilder.create());
            }
            rootBuilder.addChild(authorBuilder.create());
        }
        return rootBuilder.name("authorTree").create();
    }

    /**
     * Helper method for creating a field node with its children. Nodes of this type are used within the tables tree. They
     * define a single column of a table.
     *
     * @param name the name of the field
     * @return the field node
     */
    public static ImmutableNode createFieldNode(final String name) {
        final ImmutableNode.Builder fldBuilder = new ImmutableNode.Builder(1);
        fldBuilder.addChild(createNode("name", name));
        return fldBuilder.name("field").create();
    }

    /**
     * Helper method for creating an immutable node with a name and a value.
     *
     * @param name the node's name
     * @param value the node's value
     * @return the new node
     */
    public static ImmutableNode createNode(final String name, final Object value) {
        return new ImmutableNode.Builder().name(name).value(value).create();
    }

    /**
     * Creates a tree with a root node whose children are the test personae. Each node represents a person and has an
     * attribute pointing to the author who invented this person. There is a single child node for the associated work which
     * has again a child and an attribute.
     *
     * @return the root node of the personae tree
     */
    private static ImmutableNode createPersonaeTree() {
        final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder();
        for (int author = 0; author < AUTHORS.length; author++) {
            for (int work = 0; work < WORKS[author].length; work++) {
                for (final String person : PERSONAE[author][work]) {
                    final ImmutableNode orgValue = new ImmutableNode.Builder().name(ELEM_ORG_VALUE).value("yes").addAttribute(ATTR_TESTED, Boolean.FALSE)
                        .create();
                    final ImmutableNode workNode = new ImmutableNode.Builder(1).name(WORKS[author][work]).addChild(orgValue).create();
                    final ImmutableNode personNode = new ImmutableNode.Builder(1).name(person).addAttribute(ATTR_AUTHOR, AUTHORS[author]).addChild(workNode)
                        .create();
                    rootBuilder.addChild(personNode);
                }
            }
        }
        return rootBuilder.create();
    }

    /**
     * Creates a mock for a resolver.
     *
     * @return the resolver mock
     */
    @SuppressWarnings("unchecked")
    public static NodeKeyResolver<ImmutableNode> createResolverMock() {
        return mock(NodeKeyResolver.class);
    }

    /**
     * Creates a tree with database table data with the following structure:
     *
     * tables table name fields field name field name
     *
     * @return the resulting nodes structure
     */
    private static ImmutableNode createTablesTree() {
        return createTablesTree(TABLES, FIELDS);
    }

    /**
     * Creates as tree with database table data based on the passed in arrays of table names and fields for tables. Works
     * like the method without parameters, but allows defining the data of the structure.
     *
     * @param tables an array with the names of the tables
     * @param fields an array with the fields of the single tables
     * @return the resulting nodes structure
     */
    public static ImmutableNode createTablesTree(final String[] tables, final String[][] fields) {
        final ImmutableNode.Builder bldTables = new ImmutableNode.Builder(tables.length);
        bldTables.name("tables");
        for (int i = 0; i < tables.length; i++) {
            final ImmutableNode.Builder bldTable = new ImmutableNode.Builder(2);
            bldTable.addChild(createNode("name", tables[i]));
            final ImmutableNode.Builder bldFields = new ImmutableNode.Builder(fields[i].length);
            bldFields.name("fields");

            for (int j = 0; j < fields[i].length; j++) {
                bldFields.addChild(createFieldNode(fields[i][j]));
            }
            bldTable.addChild(bldFields.create());
            bldTables.addChild(bldTable.name("table").create());
        }
        return bldTables.create();
    }

    /**
     * Returns the name of the specified field in the tables tree.
     *
     * @param tabIdx the index of the table
     * @param fldIdx the index of the field
     * @return the name of this field
     */
    public static String field(final int tabIdx, final int fldIdx) {
        return FIELDS[tabIdx][fldIdx];
    }

    /**
     * Returns the number of fields in the test table with the given index.
     *
     * @param tabIdx the index of the table
     * @return the number of fields in this table
     */
    public static int fieldsLength(final int tabIdx) {
        return FIELDS[tabIdx].length;
    }

    /**
     * Helper method for evaluating a single component of a node key.
     *
     * @param parent the current parent node
     * @param components the array with the components of the node key
     * @param currentIdx the index of the current path component
     * @return the found target node
     * @throws NoSuchElementException if the desired node cannot be found
     */
    private static ImmutableNode findNode(final ImmutableNode parent, final String[] components, final int currentIdx) {
        if (currentIdx >= components.length) {
            return parent;
        }

        final Matcher m = PAT_KEY_WITH_INDEX.matcher(components[currentIdx]);
        final String childName;
        final int childIndex;
        if (m.matches()) {
            childName = m.group(1);
            childIndex = Integer.parseInt(m.group(2));
        } else {
            childName = components[currentIdx];
            childIndex = 0;
        }

        int foundIdx = 0;
        for (final ImmutableNode node : parent) {
            if (childName.equals(node.getNodeName()) && foundIdx++ == childIndex) {
                return findNode(node, components, currentIdx + 1);
            }
        }
        throw new NoSuchElementException("Cannot resolve child " + components[currentIdx]);
    }

    /**
     * Returns a clone of the array with the table fields. This is useful if a slightly different tree structure should be
     * created.
     *
     * @return the cloned field names
     */
    public static String[][] getClonedFields() {
        final String[][] fieldNamesNew = new String[FIELDS.length][];
        for (int i = 0; i < FIELDS.length; i++) {
            fieldNamesNew[i] = FIELDS[i].clone();
        }
        return fieldNamesNew;
    }

    /**
     * Returns a clone of the array with the table names. This is useful if a slightly different tree structure should be
     * created.
     *
     * @return the cloned table names
     */
    public static String[] getClonedTables() {
        return TABLES.clone();
    }

    /**
     * Evaluates the given key and finds the corresponding child node of the specified root. Keys have the form
     * {@code path/to/node}. If there are multiple sibling nodes with the same name, a numerical index can be specified in
     * parenthesis.
     *
     * @param root the root node
     * @param key the key to the desired node
     * @return the node with this key
     * @throws NoSuchElementException if the key cannot be resolved
     */
    public static ImmutableNode nodeForKey(final ImmutableNode root, final String key) {
        final String[] components = key.split(PATH_SEPARATOR);
        return findNode(root, components, 0);
    }

    /**
     * Evaluates the given key and finds the corresponding child node of the root node of the specified model. This is a
     * convenience method that works like the method with the same name, but obtains the root node from the given model.
     *
     * @param model the node model
     * @param key the key to the desired node
     * @return the found target node
     * @throws NoSuchElementException if the desired node cannot be found
     */
    public static ImmutableNode nodeForKey(final InMemoryNodeModel model, final String key) {
        return nodeForKey(model.getRootNode(), key);
    }

    /**
     * Evaluates the given key and finds the corresponding child node of the root node of the specified {@code NodeHandler}
     * object. This is a convenience method that works like the method with the same name, but obtains the root node from
     * the given handler object.
     *
     * @param handler the {@code NodeHandler} object
     * @param key the key to the desired node
     * @return the found target node
     * @throws NoSuchElementException if the desired node cannot be found
     */
    public static ImmutableNode nodeForKey(final NodeHandler<ImmutableNode> handler, final String key) {
        return nodeForKey(handler.getRootNode(), key);
    }

    /**
     * Convenience method for creating a path for accessing a node based on the node names.
     *
     * @param path an array with the expected node names on the path
     * @return the resulting path as string
     */
    public static String nodePath(final String... path) {
        return StringUtils.join(path, PATH_SEPARATOR);
    }

    /**
     * Convenience method for creating a node path with a special end node.
     *
     * @param endNode the name of the last path component
     * @param path an array with the expected node names on the path
     * @return the resulting path as string
     */
    public static String nodePathWithEndNode(final String endNode, final String... path) {
        return nodePath(path) + PATH_SEPARATOR + endNode;
    }

    /**
     * Returns the name of a persona.
     *
     * @param authorIdx the author index
     * @param workIdx the index of the work
     * @param personaIdx the index of the persona
     * @return the name of this persona
     */
    public static String persona(final int authorIdx, final int workIdx, final int personaIdx) {
        return PERSONAE[authorIdx][workIdx][personaIdx];
    }

    /**
     * Returns the number of personae in the given work of the specified author.
     *
     * @param authorIdx the author index
     * @param workIdx the index of the work
     * @return the number of personae in this work
     */
    public static int personaeLength(final int authorIdx, final int workIdx) {
        return PERSONAE[authorIdx][workIdx].length;
    }

    /**
     * Prepares the passed in resolver mock to resolve add keys. They are interpreted on a default expression engine.
     *
     * @param resolver the {@code NodeKeyResolver} mock
     */
    public static void prepareResolveAddKeys(final NodeKeyResolver<ImmutableNode> resolver) {
        when(resolver.resolveAddKey(any(), any(), any())).then(invocation -> {
            final ImmutableNode root = invocation.getArgument(0, ImmutableNode.class);
            final String key = invocation.getArgument(1, String.class);
            final TreeData handler = invocation.getArgument(2, TreeData.class);
            return DefaultExpressionEngine.INSTANCE.prepareAdd(root, key, handler);
        });
    }

    /**
     * Prepares a mock for a resolver to expect arbitrary resolve operations. These operations are implemented on top of a
     * default expression engine.
     *
     * @param resolver the mock resolver
     */
    @SuppressWarnings("unchecked")
    public static void prepareResolveKeyForQueries(final NodeKeyResolver<ImmutableNode> resolver) {
        when(resolver.resolveKey(any(), any(), any())).thenAnswer(invocation -> {
            final ImmutableNode root = invocation.getArgument(0, ImmutableNode.class);
            final String key = invocation.getArgument(1, String.class);
            final NodeHandler<ImmutableNode> handler = invocation.getArgument(2, NodeHandler.class);
            return DefaultExpressionEngine.INSTANCE.query(root, key, handler);
        });
    }

    /**
     * Returns the name of the test table with the given index.
     *
     * @param idx the index of the table
     * @return the name of the test table with this index
     */
    public static String table(final int idx) {
        return TABLES[idx];
    }

    /**
     * Returns the number of tables in the tables tree.
     *
     * @return the number of tables
     */
    public static int tablesLength() {
        return TABLES.length;
    }

    /**
     * Returns the work of an author with a given index.
     *
     * @param authorIdx the author index
     * @param idx the index of the work
     * @return the desired work
     */
    public static String work(final int authorIdx, final int idx) {
        return WORKS[authorIdx][idx];
    }

    /**
     * Returns the number of works for the author with the given index.
     *
     * @param authorIdx the author index
     * @return the number of works of this author
     */
    public static int worksLength(final int authorIdx) {
        return WORKS[authorIdx].length;
    }

    private NodeStructureHelper() {
        // empty
    }
}
