/* Self tests for GDB command definitions for GDB, the GNU debugger.

   Copyright (C) 2019-2026 Free Software Foundation, Inc.

   This file is part of GDB.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */

#include "cli/cli-cmds.h"
#include "cli/cli-decode.h"
#include "gdbsupport/selftest.h"
#include "gdbsupport/unordered_map.h"

namespace selftests {

/* Verify some invariants of GDB commands documentation.  */

namespace help_doc_tests {

static unsigned int nr_failed_invariants;

/* Report a broken invariant and increments nr_failed_invariants.  */

static void
broken_doc_invariant (const char *prefix, const char *name, const char *msg)
{
  gdb_printf ("help doc broken invariant: command '%s%s' help doc %s\n",
	      prefix, name, msg);
  nr_failed_invariants++;
}

/* Recursively walk the commandlist structures, and check doc invariants:
   - The first line of the doc must end with a '.'.
   - the doc must not end with a new line.
  If an invariant is not respected, produce a message and increment
  nr_failed_invariants.
  Note that we do not call SELF_CHECK in this function, as we want
  all commands to be checked before making the test fail.  */

static void
check_doc (struct cmd_list_element *commandlist, const char *prefix)
{
  struct cmd_list_element *c;

  /* Walk through the commands.  */
  for (c = commandlist; c; c = c->next)
    {
      /* Checks the doc has a first line terminated with a '.'.  */
      const char *p = c->doc;

      /* Position p on the first LF, or on terminating null byte.  */
      while (*p && *p != '\n')
	p++;
      if (p == c->doc)
	broken_doc_invariant
	  (prefix, c->name,
	   "is missing the first line terminated with a '.' character");
      else if (*(p-1) != '.')
	broken_doc_invariant
	  (prefix, c->name,
	   "first line is not terminated with a '.' character");

      /* Checks the doc is not terminated with a new line.  */
      if (c_isspace (c->doc[strlen (c->doc) - 1]))
	broken_doc_invariant
	  (prefix, c->name,
	   "has superfluous trailing whitespace");

      const char *prev_start = c->doc;
      for (const char *nl = strchr (c->doc, '\n');
	   nl != nullptr;
	   nl = strchr (prev_start, '\n'))
	{
	  if (nl == c->doc)
	    broken_doc_invariant (prefix, c->name, "has a leading newline");
	  else
	    {
	      /* \n\n is ok, so we check that explicitly here.  */
	      if (c_isspace (nl[-1]) && nl[-1] != '\n')
		broken_doc_invariant (prefix, c->name,
				      "has whitespace before a newline");
	    }

	  if (nl - prev_start > cli_help_line_length)
	    broken_doc_invariant (prefix, c->name, "has over-long line");
	  prev_start = nl + 1;
	}

      if (strlen (prev_start) > cli_help_line_length)
	broken_doc_invariant (prefix, c->name, "has over-long line");

      /* Check if this command has subcommands and is not an
	 abbreviation.  We skip checking subcommands of abbreviations
	 in order to avoid duplicates in the output.  */
      if (c->is_prefix () && !c->abbrev_flag)
	{
	  /* Recursively call ourselves on the subcommand list,
	     passing the right prefix in.  */
	  check_doc (*c->subcommands, c->prefixname ().c_str ());
	}
    }
}

static void
help_doc_invariants_tests ()
{
  nr_failed_invariants = 0;
  check_doc (cmdlist, "");
  SELF_CHECK (nr_failed_invariants == 0);
}

} /* namespace help_doc_tests */

/* Verify some invariants of GDB command structure.  */

namespace command_structure_tests {

/* Nr of commands in which a duplicated list is found.  */
static unsigned int nr_duplicates = 0;
/* Nr of commands in a list having no valid prefix cmd.  */
static unsigned int nr_invalid_prefixcmd = 0;

/* A map associating a list with the prefix leading to it.  */

static gdb::unordered_map<cmd_list_element **, const char *> lists;

/* Store each command list in lists, associated with the prefix to reach it.  A
   list must only be found once.

   Verifies that all elements of the list have the same non-null prefix
   command.  */

static void
traverse_command_structure (struct cmd_list_element **list,
			    const char *prefix)
{
  struct cmd_list_element *c, *prefixcmd;

  auto dupl = lists.find (list);
  if (dupl != lists.end ())
    {
      gdb_printf ("list %p duplicated,"
		  " reachable via prefix '%s' and '%s'."
		  "  Duplicated list first command is '%s'\n",
		  list,
		  prefix, dupl->second,
		  (*list)->name);
      nr_duplicates++;
      return;
    }

  lists.insert ({list, prefix});

  /* All commands of *list must have a prefix command equal to PREFIXCMD,
     the prefix command of the first command.  */
  if (*list == nullptr)
    prefixcmd = nullptr; /* A prefix command with an empty subcommand list.  */
  else
    prefixcmd = (*list)->prefix;

  /* Walk through the commands.  */
  for (c = *list; c; c = c->next)
    {
      /* If this command has subcommands and is not an alias,
	 traverse the subcommands.  */
      if (c->is_prefix () && !c->is_alias ())
	{
	  /* Recursively call ourselves on the subcommand list,
	     passing the right prefix in.  */
	  traverse_command_structure (c->subcommands, c->prefixname ().c_str ());
	}
      if (prefixcmd != c->prefix
	  || (prefixcmd == nullptr && *list != cmdlist))
	{
	  if (c->prefix == nullptr)
	    gdb_printf ("list %p reachable via prefix '%s'."
			"  command '%s' has null prefixcmd\n",
			list,
			prefix, c->name);
	  else
	    gdb_printf ("list %p reachable via prefix '%s'."
			"  command '%s' has a different prefixcmd\n",
			list,
			prefix, c->name);
	  nr_invalid_prefixcmd++;
	}
    }
}

/* Verify that a list of commands is present in the tree only once.  */

static void
command_structure_invariants_tests ()
{
  nr_duplicates = 0;
  nr_invalid_prefixcmd = 0;

  traverse_command_structure (&cmdlist, "");

  /* Release memory, be ready to be re-run.  */
  lists.clear ();

  SELF_CHECK (nr_duplicates == 0);
  SELF_CHECK (nr_invalid_prefixcmd == 0);
}

}

namespace essential_command_tests {

/* The maximum number of commands that can be considered
   essential by GDB.  This value was chosen arbitrarily,
   but it must be kept low, so as to not overwhelm new
   users.  */
static constexpr int max_essential_cmds = 20;

static void
essential_command_count_tests ()
{
  int nr_essential_cmds = 0;

  for (struct cmd_list_element *c = cmdlist; c != nullptr; c = c->next)
    {
      if (c->is_essential ())
	nr_essential_cmds ++;
    }

  SELF_CHECK (nr_essential_cmds <= max_essential_cmds);
}
}

} /* namespace selftests */

INIT_GDB_FILE (command_def_selftests)
{
  selftests::register_test
    ("help_doc_invariants",
     selftests::help_doc_tests::help_doc_invariants_tests);

  selftests::register_test
    ("command_structure_invariants",
     selftests::command_structure_tests::command_structure_invariants_tests);

  selftests::register_test
    ("essential_command_count",
     selftests::essential_command_tests::essential_command_count_tests);
}
