from argparse import ArgumentParser, RawTextHelpFormatter, SUPPRESS
from os import environ, getcwd

from .. import VERSION_STRING
from ..utils.cmdline import HELP_get_target_nodes
from ..utils.text import mark_for_translation as _
from .apply import bw_apply
from .debug import bw_debug
from .diff import bw_diff
from .groups import bw_groups
from .hash import bw_hash
from .items import bw_items
from .lock import bw_lock_add, bw_lock_remove, bw_lock_show
from .metadata import bw_metadata
from .nodes import bw_nodes
from .plot import bw_plot_group, bw_plot_node, bw_plot_node_groups
from .repo import bw_repo_bundle_create, bw_repo_create
from .run import bw_run
from .stats import bw_stats
from .test import bw_test
from .verify import bw_verify
from .zen import bw_zen


def build_parser_bw():
    parser = ArgumentParser(
        prog="bw",
        description=_("BundleWrap - Config Management with Python"),
    )
    parser.add_argument(
        "-a",
        "--add-host-keys",
        action='store_true',
        default=False,
        dest='add_ssh_host_keys',
        help=_("set StrictHostKeyChecking=no instead of yes for SSH"),
    )
    parser.add_argument(
        "-d",
        "--debug",
        action='store_true',
        default=False,
        dest='debug',
        help=_("print debugging info"),
    )
    parser.add_argument(
        "-r",
        "--repo-path",
        default=environ.get('BW_REPO_PATH', getcwd()),
        dest='repo_path',
        help=_("Look for repository at this path (defaults to current working directory)"),
        metavar=_("DIRECTORY"),
        type=str,
    )
    # hidden option to dump profiling info, can be inpected with
    # SnakeViz or whatever
    parser.add_argument(
        "--profile",
        default=None,
        dest='profile',
        help=SUPPRESS,
        metavar=_("FILE"),
        type=str,
    )
    parser.add_argument(
        "--version",
        action='version',
        version=VERSION_STRING,
    )
    subparsers = parser.add_subparsers(
        title=_("subcommands"),
        help=_("use 'bw <subcommand> --help' for more info"),
    )

    # bw apply
    help_apply = _("Applies the configuration defined in your repository to your nodes")
    parser_apply = subparsers.add_parser(
        "apply",
        description=help_apply,
        help=help_apply,
        formatter_class=RawTextHelpFormatter,  # for HELP_get_target_nodes
    )
    parser_apply.set_defaults(func=bw_apply)
    parser_apply.add_argument(
        'targets',
        metavar=_("TARGET"),
        nargs='+',
        type=str,
        help=HELP_get_target_nodes,
    )
    parser_apply.add_argument(
        "-D",
        "--no-diff",
        action='store_false',
        dest='show_diff',
        help=_("hide diff for incorrect items when NOT using --interactive"),
    )
    parser_apply.add_argument(
        "-f",
        "--force",
        action='store_true',
        default=False,
        dest='force',
        help=_("ignore existing hard node locks"),
    )
    parser_apply.add_argument(
        "-i",
        "--interactive",
        action='store_true',
        default=False,
        dest='interactive',
        help=_("ask before applying each item"),
    )
    parser_apply.add_argument(
        "-o",
        "--only",
        default=[],
        dest='autoonly',
        help=_("""skip all items not matching any SELECTOR:

file:/my_path     # this specific item
tag:my_tag        # items with this tag
bundle:my_bundle  # items in this bundle

dependencies of selected items will NOT be skipped
        """),
        metavar=_("SELECTOR"),
        nargs='+',
        type=str,
    )
    bw_apply_p_default = int(environ.get("BW_NODE_WORKERS", "4"))
    parser_apply.add_argument(
        "-p",
        "--parallel-nodes",
        default=bw_apply_p_default,
        dest='node_workers',
        help=_("number of nodes to apply to simultaneously "
               "(defaults to {})").format(bw_apply_p_default),
        type=int,
    )
    bw_apply_p_items_default = int(environ.get("BW_ITEM_WORKERS", "4"))
    parser_apply.add_argument(
        "-P",
        "--parallel-items",
        default=bw_apply_p_items_default,
        dest='item_workers',
        help=_("number of items to apply simultaneously on each node "
               "(defaults to {})").format(bw_apply_p_items_default),
        type=int,
    )
    parser_apply.add_argument(
        "-s",
        "--skip",
        default=[],
        dest='autoskip',
        help=_("""skip items matching any SELECTOR:

file:/my_path     # this specific item
tag:my_tag        # items with this tag
bundle:my_bundle  # items in this bundle
        """),
        metavar=_("SELECTOR"),
        nargs='+',
        type=str,
    )
    parser_apply.add_argument(
        "-S",
        "--no-summary",
        action='store_false',
        dest='summary',
        help=_("don't show stats summary"),
    )
    parser_apply.add_argument(
        "-r",
        "--resume-file",
        default=None,
        dest='resume_file',
        help=_(
            "path to a file that a list of completed nodes will be added to; "
            "if the file already exists, any nodes therein will be skipped"
        ),
        metavar=_("PATH"),
        type=str,
    )

    # bw debug
    help_debug = _("Start an interactive Python shell for this repository")
    parser_debug = subparsers.add_parser("debug", description=help_debug, help=help_debug)
    parser_debug.set_defaults(func=bw_debug)
    parser_debug.add_argument(
        "-c",
        "--command",
        default=None,
        dest='command',
        metavar=_("COMMAND"),
        required=False,
        type=str,
        help=_("command to execute in lieu of REPL"),
    )
    parser_debug.add_argument(
        "-n",
        "--node",
        default=None,
        dest='node',
        metavar=_("NODE"),
        required=False,
        type=str,
        help=_("name of node to inspect"),
    )

    # bw diff
    help_diff = _("Show differences between nodes")
    parser_diff = subparsers.add_parser(
        "diff",
        description=help_diff,
        help=help_diff,
        formatter_class=RawTextHelpFormatter,  # for HELP_get_target_nodes
    )
    parser_diff.set_defaults(func=bw_diff)
    parser_diff.add_argument(
        "-b",
        "--branch",
        default=None,
        dest='branch',
        metavar=_("REV"),
        required=False,
        type=str,
        help=_("compare with this git rev instead (requires clean working dir)"),
    )
    parser_diff.add_argument(
        "-c",
        "--cmd-change",
        default=None,
        dest='cmd_change',
        metavar=_("CMD_CHANGE"),
        required=False,
        type=str,
        help=_("command to execute between taking metadata snapshots (e.g., change Git branch)"),
    )
    parser_diff.add_argument(
        "-r",
        "--cmd-reset",
        default=None,
        dest='cmd_reset',
        metavar=_("CMD_RESET"),
        required=False,
        type=str,
        help=_("command to execute when finished (e.g., switch back to original Git branch)"),
    )
    parser_diff.add_argument(
        "-p",
        "--prompt",
        action='store_true',
        default=False,
        dest='prompt',
        help=_("interactively ask for user to make changes"),
    )
    parser_diff.add_argument(
        "-i",
        "--item",
        default=None,
        dest='item',
        metavar=_("ITEM"),
        required=False,
        type=str,
        help=_("compare this specific item between nodes"),
    )
    parser_diff.add_argument(
        "-m",
        "--metadata",
        action='store_true',
        default=False,
        dest='metadata',
        help=_("compare metadata instead of configuration"),
    )
    parser_diff.add_argument(
        'targets',
        metavar=_("TARGET"),
        nargs='+',
        type=str,
        help=HELP_get_target_nodes,
    )

    # bw groups
    help_groups = _("Lists groups in this repository")
    parser_groups = subparsers.add_parser("groups", description=help_groups, help=help_groups)
    parser_groups.set_defaults(func=bw_groups)
    parser_groups.add_argument(
        "-a", "--attrs",
        dest='attrs',
        metavar=_("ATTR"),
        nargs='+',
        type=str,
        help=_("show table with the given attributes for each group "
               "(e.g. 'all', 'members', 'os', ...)"),
    )
    parser_groups.add_argument(
        "-i",
        "--inline",
        action='store_true',
        dest='inline',
        help=_("keep lists on a single line (for grep)"),
    )
    parser_groups.add_argument(
        'groups',
        default=None,
        metavar=_("GROUP"),
        nargs='*',
        type=str,
        help=_("show the given groups (and their subgroups, unless --attrs is used)"),
    )

    # bw hash
    help_hash = _("Shows a SHA1 hash that summarizes the entire configuration for this repo, node, group, or item.")
    parser_hash = subparsers.add_parser("hash", description=help_hash, help=help_hash)
    parser_hash.set_defaults(func=bw_hash)
    parser_hash.add_argument(
        "-d",
        "--dict",
        action='store_true',
        default=False,
        dest='dict',
        help=_("instead show the data this hash is derived from"),
    )
    parser_hash.add_argument(
        "-g",
        "--group",
        action='store_true',
        default=False,
        dest='group_membership',
        help=_("hash group membership instead of configuration"),
    )
    parser_hash.add_argument(
        "-m",
        "--metadata",
        action='store_true',
        default=False,
        dest='metadata',
        help=_("hash metadata instead of configuration (not available for items)"),
    )
    parser_hash.add_argument(
        'node_or_group',
        metavar=_("NODE|GROUP"),
        type=str,
        nargs='?',
        help=_("show config hash for this node or group"),
    )
    parser_hash.add_argument(
        'item',
        metavar=_("ITEM"),
        type=str,
        nargs='?',
        help=_("show config hash for this item on the given node"),
    )

    # bw items
    help_items = _("List and preview items for a specific node")
    parser_items = subparsers.add_parser("items", description=help_items, help=help_items)
    parser_items.set_defaults(func=bw_items)
    parser_items.add_argument(
        'node',
        metavar=_("NODE"),
        type=str,
        help=_("list items for this node"),
    )
    parser_items.add_argument(
        'item',
        metavar=_("ITEM"),
        nargs='?',
        type=str,
        help=_("show configuration for this item"),
    )
    parser_items.add_argument(
        'attr',
        metavar=_("ATTRIBUTE"),
        nargs='?',
        type=str,
        help=_("show only this item attribute"),
    )
    parser_items.add_argument(
        "-f",
        "--preview",
        "--file-preview",  # TODO 4.0 remove
        action='store_true',
        dest='preview',
        help=_("print preview of given ITEM"),
    )
    parser_items.add_argument(
        "-w",
        "--write-file-previews",
        default=None,
        dest='file_preview_path',
        metavar=_("DIRECTORY"),
        required=False,
        type=str,
        help=_("create DIRECTORY and fill it with rendered file previews"),
    )
    parser_items.add_argument(
        "--attrs",
        action='store_true',
        dest='show_attrs',
        help=_("show internal item attributes"),
    )
    parser_items.add_argument(
        "--repr",
        action='store_true',
        dest='show_repr',
        help=_("show more verbose representation of each item"),
    )
    parser_items.add_argument(
        "--state",
        action='store_true',
        dest='show_sdict',
        help=_("show actual item status on node instead of should-be configuration"),
    )

    # bw lock
    help_lock = _("Manage locks on nodes used to prevent collisions between BundleWrap users")
    parser_lock = subparsers.add_parser(
        "lock",
        description=help_lock,
        help=help_lock,
    )
    parser_lock_subparsers = parser_lock.add_subparsers()

    # bw lock add
    help_lock_add = _("Add a new lock to one or more nodes")
    parser_lock_add = parser_lock_subparsers.add_parser(
        "add",
        description=help_lock_add,
        help=help_lock_add,
        formatter_class=RawTextHelpFormatter,  # for HELP_get_target_nodes
    )
    parser_lock_add.set_defaults(func=bw_lock_add)
    parser_lock_add.add_argument(
        'targets',
        metavar=_("TARGET"),
        nargs='+',
        type=str,
        help=HELP_get_target_nodes,
    )
    parser_lock_add.add_argument(
        "-c",
        "--comment",
        default="",
        dest='comment',
        help=_("brief description of the purpose of the lock"),
        type=str,
    )
    bw_lock_add_e_default = environ.get("BW_SOFTLOCK_EXPIRY", "8h")
    parser_lock_add.add_argument(
        "-e",
        "--expires-in",
        default=bw_lock_add_e_default,
        dest='expiry',
        help=_("how long before the lock is ignored and removed automatically "
               "(defaults to \"{}\")").format(bw_lock_add_e_default),
        type=str,
    )
    parser_lock_add.add_argument(
        "-i",
        "--items",
        default=["*"],
        dest='items',
        help=_("""lock only items matching any SELECTOR:

file:/my_path     # this specific item
tag:my_tag        # items with this tag
bundle:my_bundle  # items in this bundle
        """),
        metavar=_("SELECTOR"),
        nargs='+',
        type=str,
    )
    bw_lock_add_p_default = int(environ.get("BW_NODE_WORKERS", "4"))
    parser_lock_add.add_argument(
        "-p",
        "--parallel-nodes",
        default=bw_lock_add_p_default,
        dest='node_workers',
        help=_("number of nodes to lock simultaneously "
               "(defaults to {})").format(bw_lock_add_p_default),
        type=int,
    )

    # bw lock remove
    help_lock_remove = _("Remove a lock from a node")
    parser_lock_remove = parser_lock_subparsers.add_parser(
        "remove",
        description=help_lock_remove,
        help=help_lock_remove,
        formatter_class=RawTextHelpFormatter,  # for HELP_get_target_nodes
    )
    parser_lock_remove.set_defaults(func=bw_lock_remove)
    parser_lock_remove.add_argument(
        'targets',
        metavar=_("TARGET"),
        nargs='+',
        type=str,
        help=HELP_get_target_nodes,
    )
    parser_lock_remove.add_argument(
        'lock_id',
        metavar=_("LOCK_ID"),
        type=str,
        help=_("ID of the lock to remove (obtained with `bw lock show`)"),
    )
    bw_lock_remove_p_default = int(environ.get("BW_NODE_WORKERS", "4"))
    parser_lock_remove.add_argument(
        "-p",
        "--parallel-nodes",
        default=bw_lock_remove_p_default,
        dest='node_workers',
        help=_("number of nodes to remove lock from simultaneously "
               "(defaults to {})").format(bw_lock_remove_p_default),
        type=int,
    )

    # bw lock show
    help_lock_show = _("Show details of locks present on a node")
    parser_lock_show = parser_lock_subparsers.add_parser(
        "show",
        description=help_lock_show,
        help=help_lock_show,
        formatter_class=RawTextHelpFormatter,  # for HELP_get_target_nodes
    )
    parser_lock_show.set_defaults(func=bw_lock_show)
    parser_lock_show.add_argument(
        'targets',
        metavar=_("TARGETS"),
        nargs='+',
        type=str,
        help=HELP_get_target_nodes,
    )
    bw_lock_show_p_default = int(environ.get("BW_NODE_WORKERS", "4"))
    parser_lock_show.add_argument(
        "-p",
        "--parallel-nodes",
        default=bw_lock_show_p_default,
        dest='node_workers',
        help=_("number of nodes to retrieve locks from simultaneously "
               "(defaults to {})").format(bw_lock_show_p_default),
        type=int,
    )

    # bw metadata
    help_metadata = ("View a JSON representation of a node's metadata (defaults blue, reactors green, groups yellow, node red, uncolored if mixed-source) or a table of selected metadata keys from multiple nodes")
    parser_metadata = subparsers.add_parser(
        "metadata",
        description=help_metadata,
        help=help_metadata,
        formatter_class=RawTextHelpFormatter,
    )
    parser_metadata.set_defaults(func=bw_metadata)
    parser_metadata.add_argument(
        'targets',
        metavar=_("TARGET"),
        nargs='+',
        type=str,
        help=HELP_get_target_nodes,
    )
    parser_metadata.add_argument(
        "-k", "--keys",
        default=[],
        dest='keys',
        metavar=_("KEY"),
        nargs='*',
        type=str,
        help=_("show only partial metadata from the given key paths (e.g. `bw metadata mynode -k users/jdoe` to show `mynode.metadata['users']['jdoe']`)"),
    )
    parser_metadata.add_argument(
        "-b", "--blame",
        action='store_true',
        dest='blame',
        help=_("show where each piece of metadata comes from"),
    )
    parser_metadata.add_argument(
        "-D", "--hide-defaults",
        action='store_true',
        dest='hide_defaults',
        help=_("hide values set by defaults in metadata.py"),
    )
    parser_metadata.add_argument(
        "-G", "--hide-groups",
        action='store_true',
        dest='hide_groups',
        help=_("hide values set in groups.py"),
    )
    parser_metadata.add_argument(
        "-N", "--hide-node",
        action='store_true',
        dest='hide_node',
        help=_("hide values set in nodes.py"),
    )
    parser_metadata.add_argument(
        "-R", "--hide-reactors",
        action='store_true',
        dest='hide_reactors',
        help=_("hide values set by reactors in metadata.py"),
    )

    # bw nodes
    help_nodes = _("List nodes in this repository")
    parser_nodes = subparsers.add_parser(
        "nodes",
        description=help_nodes,
        help=help_nodes,
        formatter_class=RawTextHelpFormatter,  # for HELP_get_target_nodes
    )
    parser_nodes.set_defaults(func=bw_nodes)
    parser_nodes.add_argument(
        "-i",
        "--inline",
        action='store_true',
        dest='inline',
        help=_("keep lists on a single line (for grep)"),
    )
    parser_nodes.add_argument(
        'targets',
        default=None,
        metavar=_("TARGET"),
        nargs='*',
        type=str,
        help=HELP_get_target_nodes,
    )
    parser_nodes.add_argument(
        "-a",
        "--attrs",
        default=None,
        dest='attrs',
        metavar=_("ATTR"),
        nargs='+',
        type=str,
        help=_("show table with the given attributes for each node "
               "(e.g. 'all', 'groups', 'bundles', 'hostname', 'os', ...)"),
    )

    # bw plot
    help_plot = _("Generates DOT output that can be piped into `dot -Tsvg -ooutput.svg`. "
                  "The resulting output.svg can be viewed using most browsers.")
    parser_plot = subparsers.add_parser("plot", description=help_plot, help=help_plot)
    parser_plot_subparsers = parser_plot.add_subparsers()

    # bw plot group
    help_plot_group = _("Plot subgroups and node members for the given group "
                        "or the entire repository")
    parser_plot_subparsers_group = parser_plot_subparsers.add_parser(
        "group",
        description=help_plot_group,
        help=help_plot_group,
    )
    parser_plot_subparsers_group.set_defaults(func=bw_plot_group)
    parser_plot_subparsers_group.add_argument(
        'group',
        default=None,
        metavar=_("GROUP"),
        nargs='?',
        type=str,
        help=_("group to plot"),
    )
    parser_plot_subparsers_group.add_argument(
        "-N", "--no-nodes",
        action='store_false',
        dest='show_nodes',
        help=_("do not include nodes in output"),
    )

    # bw plot node
    help_plot_node = _("Plot items and their dependencies for the given node")
    parser_plot_subparsers_node = parser_plot_subparsers.add_parser(
        "node",
        description=help_plot_node,
        help=help_plot_node,
    )
    parser_plot_subparsers_node.set_defaults(func=bw_plot_node)
    parser_plot_subparsers_node.add_argument(
        'node',
        metavar=_("NODE"),
        type=str,
        help=_("node to plot"),
    )
    parser_plot_subparsers_node.add_argument(
        "--no-cluster",
        action='store_false',
        dest='cluster',
        help=_("do not cluster items by bundle"),
    )
    parser_plot_subparsers_node.add_argument(
        "--no-depends-auto",
        action='store_false',
        dest='depends_auto',
        help=_("do not show auto-generated dependencies and items"),
    )
    parser_plot_subparsers_node.add_argument(
        "--no-depends-conc",
        action='store_false',
        dest='depends_concurrency',
        help=_("do not show concurrency blocker dependencies"),
    )
    parser_plot_subparsers_node.add_argument(
        "--no-depends-regular",
        action='store_false',
        dest='depends_regular',
        help=_("do not show regular user-defined dependencies"),
    )
    parser_plot_subparsers_node.add_argument(
        "--no-depends-reverse",
        action='store_false',
        dest='depends_reverse',
        help=_("do not show reverse dependencies ('needed_by')"),
    )

    # bw plot groups-for-node
    help_plot_node_groups = _("Show where a specific node gets its groups from")
    parser_plot_subparsers_node_groups = parser_plot_subparsers.add_parser(
        "groups-for-node",
        description=help_plot_node_groups,
        help=help_plot_node_groups,
    )
    parser_plot_subparsers_node_groups.set_defaults(func=bw_plot_node_groups)
    parser_plot_subparsers_node_groups.add_argument(
        'node',
        metavar=_("NODE"),
        type=str,
        help=_("node to plot"),
    )

    # bw repo
    help_repo = _("Various subcommands to manipulate your repository")
    parser_repo = subparsers.add_parser("repo", description=help_repo, help=help_repo)
    parser_repo_subparsers = parser_repo.add_subparsers()

    # bw repo bundle
    parser_repo_subparsers_bundle = parser_repo_subparsers.add_parser("bundle")
    parser_repo_subparsers_bundle_subparsers = parser_repo_subparsers_bundle.add_subparsers()

    # bw repo bundle create
    parser_repo_subparsers_bundle_create = \
        parser_repo_subparsers_bundle_subparsers.add_parser("create")
    parser_repo_subparsers_bundle_create.set_defaults(func=bw_repo_bundle_create)
    parser_repo_subparsers_bundle_create.add_argument(
        'bundle',
        metavar=_("BUNDLE"),
        type=str,
        help=_("name of bundle to create"),
    )

    # bw repo create
    parser_repo_subparsers_create = parser_repo_subparsers.add_parser("create")
    parser_repo_subparsers_create.set_defaults(func=bw_repo_create)

    # bw run
    help_run = _("Run a one-off command on a number of nodes")
    parser_run = subparsers.add_parser(
        "run",
        description=help_run,
        help=help_run,
        formatter_class=RawTextHelpFormatter,  # for HELP_get_target_nodes
    )
    parser_run.set_defaults(func=bw_run)
    parser_run.add_argument(
        'targets',
        metavar=_("TARGET"),
        nargs='+',
        type=str,
        help=HELP_get_target_nodes,
    )
    parser_run.add_argument(
        'command',
        metavar=_("COMMAND"),
        type=str,
        help=_("command to run"),
    )
    parser_run.add_argument(
        "--stderr-table",
        action='store_true',
        dest='stderr_table',
        help=_("include command stderr in stats table"),
    )
    parser_run.add_argument(
        "--stdout-table",
        action='store_true',
        dest='stdout_table',
        help=_("include command stdout in stats table"),
    )
    bw_run_p_default = int(environ.get("BW_NODE_WORKERS", "1"))
    parser_run.add_argument(
        "-p",
        "--parallel-nodes",
        default=bw_run_p_default,
        dest='node_workers',
        help=_("number of nodes to run command on simultaneously "
               "(defaults to {})").format(bw_run_p_default),
        type=int,
    )
    parser_run.add_argument(
        "-r",
        "--resume-file",
        default=None,
        dest='resume_file',
        help=_(
            "path to a file that a list of completed nodes will be added to; "
            "if the file already exists, any nodes therein will be skipped"
        ),
        metavar=_("PATH"),
        type=str,
    )
    parser_run.add_argument(
        "-S",
        "--no-summary",
        action='store_false',
        dest='summary',
        help=_("don't show stats summary"),
    )

    # bw stats
    help_stats = _("Show some statistics about your repository")
    parser_stats = subparsers.add_parser("stats", description=help_stats, help=help_stats)
    parser_stats.set_defaults(func=bw_stats)

    # bw test
    help_test = _("Test your repository for consistency "
                  "(you can use this with a CI tool like Jenkins). "
                  "If *any* options other than -i are given, *only* the "
                  "tests selected by those options will be run. Otherwise, a "
                  "default selection of tests will be run (that selection may "
                  "change in future releases). Currently, the default is -IJKMp "
                  "if specific nodes are given and -HIJKMSp if testing the "
                  "entire repo.")
    parser_test = subparsers.add_parser(
        "test",
        description=help_test,
        help=help_test,
        formatter_class=RawTextHelpFormatter,  # for HELP_get_target_nodes
    )
    parser_test.set_defaults(func=bw_test)
    parser_test.add_argument(
        'targets',
        default=None,
        metavar=_("TARGET"),
        nargs='*',
        type=str,
        help=HELP_get_target_nodes + _("\n(defaults to all)"),
    )
    parser_test.add_argument(
        "-d",
        "--config-determinism",
        default=0,
        dest='determinism_config',
        help=_("verify determinism of configuration by running `bw hash` N times "
               "and checking for consistent results (with N > 1)"),
        metavar="N",
        type=int,
    )
    parser_test.add_argument(
        "-e",
        "--empty-groups",
        action='store_true',
        dest='empty_groups',
        help=_("check for empty groups"),
    )
    parser_test.add_argument(
        "-H",
        "--hooks-repo",
        action='store_true',
        dest='hooks_repo',
        help=_("run repo-level test hooks"),
    )
    parser_test.add_argument(
        "-i",
        "--ignore-missing-faults",
        action='store_true',
        dest='ignore_missing_faults',
        help=_("do not fail when encountering a missing Fault"),
    )
    parser_test.add_argument(
        "-I",
        "--items",
        action='store_true',
        dest='items',
        help=_("run item-level tests (like rendering templates)"),
    )
    parser_test.add_argument(
        "-J",
        "--hooks-node",
        action='store_true',
        dest='hooks_node',
        help=_("run node-level test hooks"),
    )
    parser_test.add_argument(
        "-K",
        "--metadata-keys",
        action='store_true',
        dest='metadata_keys',
        help=_("validate metadata keys"),
    )
    parser_test.add_argument(
        "-m",
        "--metadata-determinism",
        default=0,
        dest='determinism_metadata',
        help=_("verify determinism of metadata by running `bw hash -m` N times "
               "and checking for consistent results (with N > 1)"),
        metavar="N",
        type=int,
    )
    parser_test.add_argument(
        "-M",
        "--metadata-conflicts",
        action='store_true',
        dest='metadata_conflicts',
        help=_("check for conflicting metadata keys in group metadata, reactors, and defaults"),
    )
    parser_test.add_argument(
        "-o",
        "--orphaned-bundles",
        action='store_true',
        dest='orphaned_bundles',
        help=_("check for bundles not assigned to any node"),
    )
    parser_test.add_argument(
        "-p",
        "--reactor-provides",
        action='store_true',
        dest='reactor_provides',
        help=_("check for reactors returning keys other than what they declared with @metadata_reactor.provides()"),
    )
    parser_test.add_argument(
        "-q",
        "--quiet",
        action='store_true',
        dest='quiet',
        help=_("don't show successful tests"),
    )
    parser_test.add_argument(
        "-S",
        "--subgroup-loops",
        action='store_true',
        dest='subgroup_loops',
        help=_("check for loops in subgroup hierarchies"),
    )

    # bw verify
    help_verify = _("Inspect the health or 'correctness' of a node without changing it")
    parser_verify = subparsers.add_parser(
        "verify",
        description=help_verify,
        help=help_verify,
        formatter_class=RawTextHelpFormatter,  # for HELP_get_target_nodes
    )
    parser_verify.set_defaults(func=bw_verify)
    parser_verify.add_argument(
        'targets',
        metavar=_("TARGET"),
        nargs='+',
        type=str,
        help=HELP_get_target_nodes,
    )
    parser_verify.add_argument(
        "-a",
        "--show-all",
        action='store_true',
        dest='show_all',
        help=_("show correct items as well as incorrect ones"),
    )
    parser_verify.add_argument(
        "-D",
        "--no-diff",
        action='store_false',
        dest='show_diff',
        help=_("hide diff for incorrect items"),
    )
    bw_verify_p_default = int(environ.get("BW_NODE_WORKERS", "4"))
    parser_verify.add_argument(
        "-p",
        "--parallel-nodes",
        default=bw_verify_p_default,
        dest='node_workers',
        help=_("number of nodes to verify simultaneously "
               "(defaults to {})").format(bw_verify_p_default),
        type=int,
    )
    bw_verify_p_items_default = int(environ.get("BW_ITEM_WORKERS", "4"))
    parser_verify.add_argument(
        "-P",
        "--parallel-items",
        default=bw_verify_p_items_default,
        dest='item_workers',
        help=_("number of items to verify simultaneously on each node "
               "(defaults to {})").format(bw_verify_p_items_default),
        type=int,
    )
    parser_verify.add_argument(
        "-S",
        "--no-summary",
        action='store_false',
        dest='summary',
        help=_("don't show stats summary"),
    )

    # bw zen
    parser_zen = subparsers.add_parser("zen")
    parser_zen.set_defaults(func=bw_zen)
    return parser
